WIP: development_backend #4

Draft
MahmoudEmad wants to merge 26 commits from development_backend into main
42 changed files with 6263 additions and 2339 deletions

View File

@ -0,0 +1,2 @@
[net]
git-fetch-with-cli = true

286
actix_mvc_app/Cargo.lock generated
View File

@ -296,6 +296,8 @@ dependencies = [
"env_logger", "env_logger",
"futures", "futures",
"futures-util", "futures-util",
"heromodels",
"heromodels_core",
"jsonwebtoken", "jsonwebtoken",
"lazy_static", "lazy_static",
"log", "log",
@ -309,6 +311,14 @@ dependencies = [
"uuid", "uuid",
] ]
[[package]]
name = "adapter_macros"
version = "0.1.0"
dependencies = [
"chrono",
"rhai",
]
[[package]] [[package]]
name = "addr2line" name = "addr2line"
version = "0.24.2" version = "0.24.2"
@ -366,6 +376,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"const-random",
"getrandom 0.2.15",
"once_cell", "once_cell",
"version_check", "version_check",
"zerocopy 0.7.35", "zerocopy 0.7.35",
@ -478,6 +490,12 @@ version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236"
[[package]]
name = "arrayvec"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]] [[package]]
name = "async-trait" name = "async-trait"
version = "0.1.88" version = "0.1.88"
@ -547,6 +565,26 @@ dependencies = [
"zeroize", "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]] [[package]]
name = "bitflags" name = "bitflags"
version = "2.9.0" version = "2.9.0"
@ -1285,12 +1323,62 @@ dependencies = [
"hashbrown 0.14.5", "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]] [[package]]
name = "hermit-abi" name = "hermit-abi"
version = "0.3.9" version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" 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]] [[package]]
name = "hkdf" name = "hkdf"
version = "0.12.4" version = "0.12.4"
@ -1557,6 +1645,15 @@ dependencies = [
"generic-array", "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]] [[package]]
name = "is_terminal_polyfill" name = "is_terminal_polyfill"
version = "1.70.1" version = "1.70.1"
@ -1756,6 +1853,15 @@ dependencies = [
"windows-sys 0.52.0", "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]] [[package]]
name = "nom" name = "nom"
version = "7.1.3" version = "7.1.3"
@ -1824,6 +1930,9 @@ name = "once_cell"
version = "1.21.3" version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
dependencies = [
"portable-atomic",
]
[[package]] [[package]]
name = "opaque-debug" name = "opaque-debug"
@ -1841,6 +1950,16 @@ dependencies = [
"hashbrown 0.14.5", "hashbrown 0.14.5",
] ]
[[package]]
name = "ourdb"
version = "0.1.0"
dependencies = [
"crc32fast",
"log",
"rand 0.8.5",
"thiserror 1.0.69",
]
[[package]] [[package]]
name = "parking_lot" name = "parking_lot"
version = "0.12.3" version = "0.12.3"
@ -1907,7 +2026,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "198db74531d58c70a361c42201efde7e2591e976d518caf7662a47dc5720e7b6" checksum = "198db74531d58c70a361c42201efde7e2591e976d518caf7662a47dc5720e7b6"
dependencies = [ dependencies = [
"memchr", "memchr",
"thiserror", "thiserror 2.0.12",
"ucd-trie", "ucd-trie",
] ]
@ -2210,6 +2329,75 @@ version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 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]] [[package]]
name = "ring" name = "ring"
version = "0.16.20" version = "0.16.20"
@ -2247,6 +2435,16 @@ dependencies = [
"ordered-multimap", "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]] [[package]]
name = "rustc-demangle" name = "rustc-demangle"
version = "0.1.24" version = "0.1.24"
@ -2421,7 +2619,7 @@ checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb"
dependencies = [ dependencies = [
"num-bigint", "num-bigint",
"num-traits", "num-traits",
"thiserror", "thiserror 2.0.12",
"time", "time",
] ]
@ -2456,6 +2654,17 @@ version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9"
[[package]]
name = "smartstring"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29"
dependencies = [
"autocfg",
"static_assertions",
"version_check",
]
[[package]] [[package]]
name = "socket2" name = "socket2"
version = "0.4.10" version = "0.4.10"
@ -2488,12 +2697,37 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]]
name = "static_assertions"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]] [[package]]
name = "strsim" name = "strsim"
version = "0.11.1" version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 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]] [[package]]
name = "subtle" name = "subtle"
version = "2.6.1" version = "2.6.1"
@ -2557,13 +2791,39 @@ dependencies = [
"unic-segment", "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]] [[package]]
name = "thiserror" name = "thiserror"
version = "2.0.12" version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
dependencies = [ 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]] [[package]]
@ -2723,6 +2983,14 @@ dependencies = [
"once_cell", "once_cell",
] ]
[[package]]
name = "tst"
version = "0.1.0"
dependencies = [
"ourdb",
"thiserror 1.0.69",
]
[[package]] [[package]]
name = "typenum" name = "typenum"
version = "1.18.0" version = "1.18.0"
@ -2831,6 +3099,12 @@ version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
[[package]]
name = "unty"
version = "0.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae"
[[package]] [[package]]
name = "url" name = "url"
version = "2.5.4" version = "2.5.4"
@ -2888,6 +3162,12 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "virtue"
version = "0.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1"
[[package]] [[package]]
name = "walkdir" name = "walkdir"
version = "2.5.0" version = "2.5.0"

View File

@ -15,6 +15,8 @@ env_logger = "0.11.2"
log = "0.4.21" log = "0.4.21"
dotenv = "0.15.0" dotenv = "0.15.0"
chrono = { version = "0.4.35", features = ["serde"] } chrono = { version = "0.4.35", features = ["serde"] }
heromodels = { path = "../../db/heromodels" }
heromodels_core = { path = "../../db/heromodels_core" }
config = "0.14.0" config = "0.14.0"
num_cpus = "1.16.0" num_cpus = "1.16.0"
futures = "0.3.30" futures = "0.3.30"
@ -27,3 +29,8 @@ redis = { version = "0.23.0", features = ["tokio-comp"] }
jsonwebtoken = "8.3.0" jsonwebtoken = "8.3.0"
pulldown-cmark = "0.13.0" pulldown-cmark = "0.13.0"
urlencoding = "2.1.3" 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" }

View File

@ -1,6 +1,6 @@
use std::env;
use config::{Config, ConfigError, File}; use config::{Config, ConfigError, File};
use serde::Deserialize; use serde::Deserialize;
use std::env;
/// Application configuration /// Application configuration
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Clone)]
@ -13,6 +13,7 @@ pub struct AppConfig {
/// Server configuration /// Server configuration
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Clone)]
#[allow(dead_code)]
pub struct ServerConfig { pub struct ServerConfig {
/// Host address to bind to /// Host address to bind to
pub host: String, pub host: String,
@ -50,7 +51,8 @@ impl AppConfig {
} }
// Override with environment variables (e.g., SERVER__HOST, SERVER__PORT) // 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 // Build and deserialize the config
let config = config_builder.build()?; let config = config_builder.build()?;

View File

@ -1,12 +1,13 @@
use actix_web::{web, HttpResponse, Result}; use actix_web::{HttpResponse, Result, web};
use tera::{Context, Tera}; use chrono::{Duration, Utc};
use chrono::{Utc, Duration};
use serde::Deserialize; use serde::Deserialize;
use tera::{Context, Tera};
use crate::models::asset::{Asset, AssetType, AssetStatus, BlockchainInfo, ValuationPoint, AssetTransaction, AssetStatistics}; use crate::models::asset::{Asset, AssetStatistics, AssetStatus, AssetType, BlockchainInfo};
use crate::utils::render_template; use crate::utils::render_template;
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub struct AssetForm { pub struct AssetForm {
pub name: String, pub name: String,
pub description: String, pub description: String,
@ -14,6 +15,7 @@ pub struct AssetForm {
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub struct ValuationForm { pub struct ValuationForm {
pub value: f64, pub value: f64,
pub currency: String, pub currency: String,
@ -22,6 +24,7 @@ pub struct ValuationForm {
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub struct TransactionForm { pub struct TransactionForm {
pub transaction_type: String, pub transaction_type: String,
pub from_address: Option<String>, pub from_address: Option<String>,
@ -80,10 +83,19 @@ impl AssetController {
.map(|asset_type| { .map(|asset_type| {
let mut map = serde_json::Map::new(); let mut map = serde_json::Map::new();
let type_str = asset_type.as_str(); let type_str = asset_type.as_str();
let count = assets.iter().filter(|a| a.asset_type == *asset_type).count(); let count = assets
.iter()
.filter(|a| a.asset_type == *asset_type)
.count();
map.insert("type".to_string(), serde_json::Value::String(type_str.to_string())); map.insert(
map.insert("count".to_string(), serde_json::Value::Number(serde_json::Number::from(count))); "type".to_string(),
serde_json::Value::String(type_str.to_string()),
);
map.insert(
"count".to_string(),
serde_json::Value::Number(serde_json::Number::from(count)),
);
map map
}) })
@ -106,10 +118,8 @@ impl AssetController {
let assets = Self::get_mock_assets(); let assets = Self::get_mock_assets();
println!("DEBUG: Generated {} mock assets", assets.len()); println!("DEBUG: Generated {} mock assets", assets.len());
let assets_data: Vec<serde_json::Map<String, serde_json::Value>> = assets let assets_data: Vec<serde_json::Map<String, serde_json::Value>> =
.iter() assets.iter().map(|a| Self::asset_to_json(a)).collect();
.map(|a| Self::asset_to_json(a))
.collect();
// Add active_page for navigation highlighting // Add active_page for navigation highlighting
context.insert("active_page", &"assets"); context.insert("active_page", &"assets");
@ -132,10 +142,8 @@ impl AssetController {
let assets = Self::get_mock_assets(); let assets = Self::get_mock_assets();
println!("DEBUG: Generated {} mock assets", assets.len()); println!("DEBUG: Generated {} mock assets", assets.len());
let assets_data: Vec<serde_json::Map<String, serde_json::Value>> = assets let assets_data: Vec<serde_json::Map<String, serde_json::Value>> =
.iter() assets.iter().map(|a| Self::asset_to_json(a)).collect();
.map(|a| Self::asset_to_json(a))
.collect();
// Add active_page for navigation highlighting // Add active_page for navigation highlighting
context.insert("active_page", &"assets"); context.insert("active_page", &"assets");
@ -177,9 +185,20 @@ impl AssetController {
.iter() .iter()
.map(|v| { .map(|v| {
let mut map = serde_json::Map::new(); let mut map = serde_json::Map::new();
map.insert("date".to_string(), serde_json::Value::String(v.date.format("%Y-%m-%d").to_string())); map.insert(
map.insert("value".to_string(), serde_json::Value::Number(serde_json::Number::from_f64(v.value).unwrap())); "date".to_string(),
map.insert("currency".to_string(), serde_json::Value::String(v.currency.clone())); serde_json::Value::String(v.date.format("%Y-%m-%d").to_string()),
);
map.insert(
"value".to_string(),
serde_json::Value::Number(
serde_json::Number::from_f64(v.value).unwrap(),
),
);
map.insert(
"currency".to_string(),
serde_json::Value::String(v.currency.clone()),
);
map map
}) })
.collect(); .collect();
@ -190,7 +209,7 @@ impl AssetController {
let response = render_template(&tmpl, "assets/detail.html", &context); let response = render_template(&tmpl, "assets/detail.html", &context);
println!("DEBUG: Finished rendering asset detail template"); println!("DEBUG: Finished rendering asset detail template");
response response
}, }
None => { None => {
println!("DEBUG: Asset not found with ID {}", asset_id); println!("DEBUG: Asset not found with ID {}", asset_id);
Ok(HttpResponse::NotFound().finish()) Ok(HttpResponse::NotFound().finish())
@ -216,7 +235,7 @@ impl AssetController {
("Share", "Share"), ("Share", "Share"),
("Bond", "Bond"), ("Bond", "Bond"),
("IntellectualProperty", "Intellectual Property"), ("IntellectualProperty", "Intellectual Property"),
("Other", "Other") ("Other", "Other"),
]; ];
context.insert("asset_types", &asset_types); context.insert("asset_types", &asset_types);
@ -237,7 +256,9 @@ impl AssetController {
// In a real application, we would save the asset to the database // In a real application, we would save the asset to the database
// For now, we'll just redirect to the assets list // For now, we'll just redirect to the assets list
Ok(HttpResponse::Found().append_header(("Location", "/assets")).finish()) Ok(HttpResponse::Found()
.append_header(("Location", "/assets"))
.finish())
} }
// Add a valuation to an asset // Add a valuation to an asset
@ -253,7 +274,9 @@ impl AssetController {
// In a real application, we would update the asset in the database // In a real application, we would update the asset in the database
// For now, we'll just redirect to the asset detail page // For now, we'll just redirect to the asset detail page
Ok(HttpResponse::Found().append_header(("Location", format!("/assets/{}", asset_id))).finish()) Ok(HttpResponse::Found()
.append_header(("Location", format!("/assets/{}", asset_id)))
.finish())
} }
// Add a transaction to an asset // Add a transaction to an asset
@ -269,7 +292,9 @@ impl AssetController {
// In a real application, we would update the asset in the database // In a real application, we would update the asset in the database
// For now, we'll just redirect to the asset detail page // For now, we'll just redirect to the asset detail page
Ok(HttpResponse::Found().append_header(("Location", format!("/assets/{}", asset_id))).finish()) Ok(HttpResponse::Found()
.append_header(("Location", format!("/assets/{}", asset_id)))
.finish())
} }
// Update the status of an asset // Update the status of an asset
@ -284,7 +309,9 @@ impl AssetController {
// In a real application, we would update the asset in the database // In a real application, we would update the asset in the database
// For now, we'll just redirect to the asset detail page // For now, we'll just redirect to the asset detail page
Ok(HttpResponse::Found().append_header(("Location", format!("/assets/{}", asset_id))).finish()) Ok(HttpResponse::Found()
.append_header(("Location", format!("/assets/{}", asset_id)))
.finish())
} }
// Test method to render a simple test page // Test method to render a simple test page
@ -325,118 +352,233 @@ impl AssetController {
fn asset_to_json(asset: &Asset) -> serde_json::Map<String, serde_json::Value> { fn asset_to_json(asset: &Asset) -> serde_json::Map<String, serde_json::Value> {
let mut map = serde_json::Map::new(); let mut map = serde_json::Map::new();
map.insert("id".to_string(), serde_json::Value::String(asset.id.clone())); map.insert(
map.insert("name".to_string(), serde_json::Value::String(asset.name.clone())); "id".to_string(),
map.insert("description".to_string(), serde_json::Value::String(asset.description.clone())); serde_json::Value::String(asset.id.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(
map.insert("owner_id".to_string(), serde_json::Value::String(asset.owner_id.clone())); "name".to_string(),
map.insert("owner_name".to_string(), serde_json::Value::String(asset.owner_name.clone())); serde_json::Value::String(asset.name.clone()),
map.insert("created_at".to_string(), serde_json::Value::String(asset.created_at.format("%Y-%m-%d").to_string())); );
map.insert("updated_at".to_string(), serde_json::Value::String(asset.updated_at.format("%Y-%m-%d").to_string())); 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(
"owner_id".to_string(),
serde_json::Value::String(asset.owner_id.clone()),
);
map.insert(
"owner_name".to_string(),
serde_json::Value::String(asset.owner_name.clone()),
);
map.insert(
"created_at".to_string(),
serde_json::Value::String(asset.created_at.format("%Y-%m-%d").to_string()),
);
map.insert(
"updated_at".to_string(),
serde_json::Value::String(asset.updated_at.format("%Y-%m-%d").to_string()),
);
// Add current valuation if available // Add current valuation if available
if let Some(current_valuation) = asset.current_valuation { if let Some(current_valuation) = asset.current_valuation {
map.insert("current_valuation".to_string(), serde_json::Value::Number(serde_json::Number::from_f64(current_valuation).unwrap())); map.insert(
"current_valuation".to_string(),
serde_json::Value::Number(serde_json::Number::from_f64(current_valuation).unwrap()),
);
if let Some(valuation_currency) = &asset.valuation_currency { if let Some(valuation_currency) = &asset.valuation_currency {
map.insert("valuation_currency".to_string(), serde_json::Value::String(valuation_currency.clone())); map.insert(
"valuation_currency".to_string(),
serde_json::Value::String(valuation_currency.clone()),
);
} }
if let Some(valuation_date) = asset.valuation_date { if let Some(valuation_date) = asset.valuation_date {
map.insert("valuation_date".to_string(), serde_json::Value::String(valuation_date.format("%Y-%m-%d").to_string())); map.insert(
"valuation_date".to_string(),
serde_json::Value::String(valuation_date.format("%Y-%m-%d").to_string()),
);
} }
} }
// Add blockchain info if available // Add blockchain info if available
if let Some(blockchain_info) = &asset.blockchain_info { if let Some(blockchain_info) = &asset.blockchain_info {
let mut blockchain_map = serde_json::Map::new(); let mut blockchain_map = serde_json::Map::new();
blockchain_map.insert("blockchain".to_string(), serde_json::Value::String(blockchain_info.blockchain.clone())); blockchain_map.insert(
blockchain_map.insert("token_id".to_string(), serde_json::Value::String(blockchain_info.token_id.clone())); "blockchain".to_string(),
blockchain_map.insert("contract_address".to_string(), serde_json::Value::String(blockchain_info.contract_address.clone())); serde_json::Value::String(blockchain_info.blockchain.clone()),
blockchain_map.insert("owner_address".to_string(), serde_json::Value::String(blockchain_info.owner_address.clone())); );
blockchain_map.insert(
"token_id".to_string(),
serde_json::Value::String(blockchain_info.token_id.clone()),
);
blockchain_map.insert(
"contract_address".to_string(),
serde_json::Value::String(blockchain_info.contract_address.clone()),
);
blockchain_map.insert(
"owner_address".to_string(),
serde_json::Value::String(blockchain_info.owner_address.clone()),
);
if let Some(transaction_hash) = &blockchain_info.transaction_hash { if let Some(transaction_hash) = &blockchain_info.transaction_hash {
blockchain_map.insert("transaction_hash".to_string(), serde_json::Value::String(transaction_hash.clone())); blockchain_map.insert(
"transaction_hash".to_string(),
serde_json::Value::String(transaction_hash.clone()),
);
} }
if let Some(block_number) = blockchain_info.block_number { if let Some(block_number) = blockchain_info.block_number {
blockchain_map.insert("block_number".to_string(), serde_json::Value::Number(serde_json::Number::from(block_number))); blockchain_map.insert(
"block_number".to_string(),
serde_json::Value::Number(serde_json::Number::from(block_number)),
);
} }
if let Some(timestamp) = blockchain_info.timestamp { if let Some(timestamp) = blockchain_info.timestamp {
blockchain_map.insert("timestamp".to_string(), serde_json::Value::String(timestamp.format("%Y-%m-%d").to_string())); blockchain_map.insert(
"timestamp".to_string(),
serde_json::Value::String(timestamp.format("%Y-%m-%d").to_string()),
);
} }
map.insert("blockchain_info".to_string(), serde_json::Value::Object(blockchain_map)); map.insert(
"blockchain_info".to_string(),
serde_json::Value::Object(blockchain_map),
);
} }
// Add valuation history // Add valuation history
let valuation_history: Vec<serde_json::Value> = asset.valuation_history.iter() let valuation_history: Vec<serde_json::Value> = asset
.valuation_history
.iter()
.map(|v| { .map(|v| {
let mut valuation_map = serde_json::Map::new(); let mut valuation_map = serde_json::Map::new();
valuation_map.insert("id".to_string(), serde_json::Value::String(v.id.clone())); valuation_map.insert("id".to_string(), serde_json::Value::String(v.id.clone()));
valuation_map.insert("date".to_string(), serde_json::Value::String(v.date.format("%Y-%m-%d").to_string())); valuation_map.insert(
valuation_map.insert("value".to_string(), serde_json::Value::Number(serde_json::Number::from_f64(v.value).unwrap())); "date".to_string(),
valuation_map.insert("currency".to_string(), serde_json::Value::String(v.currency.clone())); serde_json::Value::String(v.date.format("%Y-%m-%d").to_string()),
valuation_map.insert("source".to_string(), serde_json::Value::String(v.source.clone())); );
valuation_map.insert(
"value".to_string(),
serde_json::Value::Number(serde_json::Number::from_f64(v.value).unwrap()),
);
valuation_map.insert(
"currency".to_string(),
serde_json::Value::String(v.currency.clone()),
);
valuation_map.insert(
"source".to_string(),
serde_json::Value::String(v.source.clone()),
);
if let Some(notes) = &v.notes { if let Some(notes) = &v.notes {
valuation_map.insert("notes".to_string(), serde_json::Value::String(notes.clone())); valuation_map.insert(
"notes".to_string(),
serde_json::Value::String(notes.clone()),
);
} }
serde_json::Value::Object(valuation_map) serde_json::Value::Object(valuation_map)
}) })
.collect(); .collect();
map.insert("valuation_history".to_string(), serde_json::Value::Array(valuation_history)); map.insert(
"valuation_history".to_string(),
serde_json::Value::Array(valuation_history),
);
// Add transaction history // Add transaction history
let transaction_history: Vec<serde_json::Value> = asset.transaction_history.iter() let transaction_history: Vec<serde_json::Value> = asset
.transaction_history
.iter()
.map(|t| { .map(|t| {
let mut transaction_map = serde_json::Map::new(); let mut transaction_map = serde_json::Map::new();
transaction_map.insert("id".to_string(), serde_json::Value::String(t.id.clone())); transaction_map.insert("id".to_string(), serde_json::Value::String(t.id.clone()));
transaction_map.insert("transaction_type".to_string(), serde_json::Value::String(t.transaction_type.clone())); transaction_map.insert(
transaction_map.insert("date".to_string(), serde_json::Value::String(t.date.format("%Y-%m-%d").to_string())); "transaction_type".to_string(),
serde_json::Value::String(t.transaction_type.clone()),
);
transaction_map.insert(
"date".to_string(),
serde_json::Value::String(t.date.format("%Y-%m-%d").to_string()),
);
if let Some(from_address) = &t.from_address { if let Some(from_address) = &t.from_address {
transaction_map.insert("from_address".to_string(), serde_json::Value::String(from_address.clone())); transaction_map.insert(
"from_address".to_string(),
serde_json::Value::String(from_address.clone()),
);
} }
if let Some(to_address) = &t.to_address { if let Some(to_address) = &t.to_address {
transaction_map.insert("to_address".to_string(), serde_json::Value::String(to_address.clone())); transaction_map.insert(
"to_address".to_string(),
serde_json::Value::String(to_address.clone()),
);
} }
if let Some(amount) = t.amount { if let Some(amount) = t.amount {
transaction_map.insert("amount".to_string(), serde_json::Value::Number(serde_json::Number::from_f64(amount).unwrap())); transaction_map.insert(
"amount".to_string(),
serde_json::Value::Number(serde_json::Number::from_f64(amount).unwrap()),
);
} }
if let Some(currency) = &t.currency { if let Some(currency) = &t.currency {
transaction_map.insert("currency".to_string(), serde_json::Value::String(currency.clone())); transaction_map.insert(
"currency".to_string(),
serde_json::Value::String(currency.clone()),
);
} }
if let Some(transaction_hash) = &t.transaction_hash { if let Some(transaction_hash) = &t.transaction_hash {
transaction_map.insert("transaction_hash".to_string(), serde_json::Value::String(transaction_hash.clone())); transaction_map.insert(
"transaction_hash".to_string(),
serde_json::Value::String(transaction_hash.clone()),
);
} }
if let Some(notes) = &t.notes { if let Some(notes) = &t.notes {
transaction_map.insert("notes".to_string(), serde_json::Value::String(notes.clone())); transaction_map.insert(
"notes".to_string(),
serde_json::Value::String(notes.clone()),
);
} }
serde_json::Value::Object(transaction_map) serde_json::Value::Object(transaction_map)
}) })
.collect(); .collect();
map.insert("transaction_history".to_string(), serde_json::Value::Array(transaction_history)); map.insert(
"transaction_history".to_string(),
serde_json::Value::Array(transaction_history),
);
// Add image URL if available // Add image URL if available
if let Some(image_url) = &asset.image_url { if let Some(image_url) = &asset.image_url {
map.insert("image_url".to_string(), serde_json::Value::String(image_url.clone())); map.insert(
"image_url".to_string(),
serde_json::Value::String(image_url.clone()),
);
} }
// Add external URL if available // Add external URL if available
if let Some(external_url) = &asset.external_url { if let Some(external_url) = &asset.external_url {
map.insert("external_url".to_string(), serde_json::Value::String(external_url.clone())); map.insert(
"external_url".to_string(),
serde_json::Value::String(external_url.clone()),
);
} }
map map
@ -481,14 +623,31 @@ impl AssetController {
token_id: "ZRESORT".to_string(), token_id: "ZRESORT".to_string(),
contract_address: "0x123456789abcdef123456789abcdef12345678ab".to_string(), contract_address: "0x123456789abcdef123456789abcdef12345678ab".to_string(),
owner_address: "0xc3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4".to_string(), owner_address: "0xc3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4".to_string(),
transaction_hash: Some("0xabcdef123456789abcdef123456789abcdef123456789abcdef123456789abcd".to_string()), transaction_hash: Some(
"0xabcdef123456789abcdef123456789abcdef123456789abcdef123456789abcd".to_string(),
),
block_number: Some(9876543), block_number: Some(9876543),
timestamp: Some(now - Duration::days(120)), timestamp: Some(now - Duration::days(120)),
}); });
zanzibar_resort.add_valuation(650000.0, "USD", "ZDFZ Property Registry", Some("Initial tokenization valuation".to_string())); zanzibar_resort.add_valuation(
zanzibar_resort.add_valuation(700000.0, "USD", "International Property Appraisers", Some("Independent third-party valuation".to_string())); 650000.0,
zanzibar_resort.add_valuation(750000.0, "USD", "ZDFZ Property Registry", Some("Updated valuation after infrastructure improvements".to_string())); "USD",
"ZDFZ Property Registry",
Some("Initial tokenization valuation".to_string()),
);
zanzibar_resort.add_valuation(
700000.0,
"USD",
"International Property Appraisers",
Some("Independent third-party valuation".to_string()),
);
zanzibar_resort.add_valuation(
750000.0,
"USD",
"ZDFZ Property Registry",
Some("Updated valuation after infrastructure improvements".to_string()),
);
zanzibar_resort.add_transaction( zanzibar_resort.add_transaction(
"Tokenization", "Tokenization",
@ -550,9 +709,24 @@ impl AssetController {
timestamp: Some(now - Duration::days(365)), timestamp: Some(now - Duration::days(365)),
}); });
zaz_token.add_valuation(300000.0, "USD", "ZDFZ Token Exchange", Some("Initial valuation at launch".to_string())); zaz_token.add_valuation(
zaz_token.add_valuation(320000.0, "USD", "ZDFZ Token Exchange", Some("Valuation after successful governance implementation".to_string())); 300000.0,
zaz_token.add_valuation(350000.0, "USD", "ZDFZ Token Exchange", Some("Current market valuation".to_string())); "USD",
"ZDFZ Token Exchange",
Some("Initial valuation at launch".to_string()),
);
zaz_token.add_valuation(
320000.0,
"USD",
"ZDFZ Token Exchange",
Some("Valuation after successful governance implementation".to_string()),
);
zaz_token.add_valuation(
350000.0,
"USD",
"ZDFZ Token Exchange",
Some("Current market valuation".to_string()),
);
zaz_token.add_transaction( zaz_token.add_transaction(
"Distribution", "Distribution",
@ -610,14 +784,31 @@ impl AssetController {
token_id: "SPICE".to_string(), token_id: "SPICE".to_string(),
contract_address: "0x3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4".to_string(), contract_address: "0x3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4".to_string(),
owner_address: "0x6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b".to_string(), owner_address: "0x6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b".to_string(),
transaction_hash: Some("0x789abcdef123456789abcdef123456789abcdef123456789abcdef123456789a".to_string()), transaction_hash: Some(
"0x789abcdef123456789abcdef123456789abcdef123456789abcdef123456789a".to_string(),
),
block_number: Some(7654321), block_number: Some(7654321),
timestamp: Some(now - Duration::days(180)), timestamp: Some(now - Duration::days(180)),
}); });
spice_trade_shares.add_valuation(150000.0, "USD", "ZDFZ Business Registry", Some("Initial company valuation at incorporation".to_string())); spice_trade_shares.add_valuation(
spice_trade_shares.add_valuation(175000.0, "USD", "ZDFZ Business Registry", Some("Valuation after first export contracts".to_string())); 150000.0,
spice_trade_shares.add_valuation(200000.0, "USD", "ZDFZ Business Registry", Some("Current valuation after expansion to European markets".to_string())); "USD",
"ZDFZ Business Registry",
Some("Initial company valuation at incorporation".to_string()),
);
spice_trade_shares.add_valuation(
175000.0,
"USD",
"ZDFZ Business Registry",
Some("Valuation after first export contracts".to_string()),
);
spice_trade_shares.add_valuation(
200000.0,
"USD",
"ZDFZ Business Registry",
Some("Current valuation after expansion to European markets".to_string()),
);
spice_trade_shares.add_transaction( spice_trade_shares.add_transaction(
"Share Issuance", "Share Issuance",
@ -675,14 +866,31 @@ impl AssetController {
token_id: "TIDALIP".to_string(), token_id: "TIDALIP".to_string(),
contract_address: "0x2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3".to_string(), contract_address: "0x2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3".to_string(),
owner_address: "0x4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f".to_string(), owner_address: "0x4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f".to_string(),
transaction_hash: Some("0x56789abcdef123456789abcdef123456789abcdef123456789abcdef12345678".to_string()), transaction_hash: Some(
"0x56789abcdef123456789abcdef123456789abcdef123456789abcdef12345678".to_string(),
),
block_number: Some(5432109), block_number: Some(5432109),
timestamp: Some(now - Duration::days(120)), timestamp: Some(now - Duration::days(120)),
}); });
tidal_energy_patent.add_valuation(80000.0, "USD", "ZDFZ IP Registry", Some("Initial patent valuation upon filing".to_string())); tidal_energy_patent.add_valuation(
tidal_energy_patent.add_valuation(100000.0, "USD", "ZDFZ IP Registry", Some("Valuation after successful prototype testing".to_string())); 80000.0,
tidal_energy_patent.add_valuation(120000.0, "USD", "ZDFZ IP Registry", Some("Current valuation after pilot implementation".to_string())); "USD",
"ZDFZ IP Registry",
Some("Initial patent valuation upon filing".to_string()),
);
tidal_energy_patent.add_valuation(
100000.0,
"USD",
"ZDFZ IP Registry",
Some("Valuation after successful prototype testing".to_string()),
);
tidal_energy_patent.add_valuation(
120000.0,
"USD",
"ZDFZ IP Registry",
Some("Current valuation after pilot implementation".to_string()),
);
tidal_energy_patent.add_transaction( tidal_energy_patent.add_transaction(
"Registration", "Registration",
@ -740,14 +948,31 @@ impl AssetController {
token_id: "HERITAGE1".to_string(), token_id: "HERITAGE1".to_string(),
contract_address: "0x06012c8cf97BEaD5deAe237070F9587f8E7A266d".to_string(), contract_address: "0x06012c8cf97BEaD5deAe237070F9587f8E7A266d".to_string(),
owner_address: "0xb794f5ea0ba39494ce839613fffba74279579268".to_string(), owner_address: "0xb794f5ea0ba39494ce839613fffba74279579268".to_string(),
transaction_hash: Some("0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234".to_string()), transaction_hash: Some(
"0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234".to_string(),
),
block_number: Some(12345678), block_number: Some(12345678),
timestamp: Some(now - Duration::days(90)), timestamp: Some(now - Duration::days(90)),
}); });
zanzibar_heritage_nft.add_valuation(5000.0, "USD", "ZDFZ Artwork Marketplace", Some("Initial offering price".to_string())); zanzibar_heritage_nft.add_valuation(
zanzibar_heritage_nft.add_valuation(5500.0, "USD", "ZDFZ Artwork Marketplace", Some("Valuation after artist exhibition".to_string())); 5000.0,
zanzibar_heritage_nft.add_valuation(6000.0, "USD", "ZDFZ Artwork Marketplace", Some("Current market valuation".to_string())); "USD",
"ZDFZ Artwork Marketplace",
Some("Initial offering price".to_string()),
);
zanzibar_heritage_nft.add_valuation(
5500.0,
"USD",
"ZDFZ Artwork Marketplace",
Some("Valuation after artist exhibition".to_string()),
);
zanzibar_heritage_nft.add_valuation(
6000.0,
"USD",
"ZDFZ Artwork Marketplace",
Some("Current market valuation".to_string()),
);
zanzibar_heritage_nft.add_transaction( zanzibar_heritage_nft.add_transaction(
"Minting", "Minting",

View File

@ -25,6 +25,7 @@ lazy_static! {
/// Controller for handling authentication-related routes /// Controller for handling authentication-related routes
pub struct AuthController; pub struct AuthController;
#[allow(dead_code)]
impl AuthController { impl AuthController {
/// Generate a JWT token for a user /// Generate a JWT token for a user
fn generate_token(email: &str, role: &UserRole) -> Result<String, jsonwebtoken::errors::Error> { fn generate_token(email: &str, role: &UserRole) -> Result<String, jsonwebtoken::errors::Error> {

View File

@ -1,12 +1,17 @@
use actix_web::{web, HttpResponse, Responder, Result};
use actix_session::Session; use actix_session::Session;
use actix_web::{HttpResponse, Responder, Result, web};
use chrono::{DateTime, Datelike, NaiveDate, TimeZone, Utc}; use chrono::{DateTime, Datelike, NaiveDate, TimeZone, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tera::Tera;
use serde_json::Value; use serde_json::Value;
use tera::Tera;
use crate::models::{CalendarEvent, CalendarViewMode}; use crate::db::calendar::{
use crate::utils::{RedisCalendarService, render_template}; 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 /// Controller for handling calendar-related routes
pub struct CalendarController; pub struct CalendarController;
@ -14,9 +19,11 @@ pub struct CalendarController;
impl CalendarController { impl CalendarController {
/// Helper function to get user from session /// Helper function to get user from session
fn get_user_from_session(session: &Session) -> Option<Value> { fn get_user_from_session(session: &Session) -> Option<Value> {
session.get::<String>("user").ok().flatten().and_then(|user_json| { session
serde_json::from_str(&user_json).ok() .get::<String>("user")
}) .ok()
.flatten()
.and_then(|user_json| serde_json::from_str(&user_json).ok())
} }
/// Handles the calendar page route /// Handles the calendar page route
@ -29,13 +36,16 @@ impl CalendarController {
ctx.insert("active_page", "calendar"); ctx.insert("active_page", "calendar");
// Parse the view mode from the query parameters // 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()); ctx.insert("view_mode", &view_mode.to_str());
// Parse the date from the query parameters or use the current date // Parse the date from the query parameters or use the current date
let date = if let Some(date_str) = &query.date { let date = if let Some(date_str) = &query.date {
match NaiveDate::parse_from_str(date_str, "%Y-%m-%d") { 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(), Err(_) => Utc::now(),
} }
} else { } else {
@ -47,44 +57,99 @@ impl CalendarController {
ctx.insert("current_month", &date.month()); ctx.insert("current_month", &date.month());
ctx.insert("current_day", &date.day()); 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) { if let Some(user) = Self::get_user_from_session(&_session) {
ctx.insert("user", &user); 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 // Get events for the current view
let (start_date, end_date) = match view_mode { let (start_date, end_date) = match view_mode {
CalendarViewMode::Year => { CalendarViewMode::Year => {
let start = Utc.with_ymd_and_hms(date.year(), 1, 1, 0, 0, 0).unwrap(); 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) (start, end)
}, }
CalendarViewMode::Month => { 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 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) (start, end)
}, }
CalendarViewMode::Week => { CalendarViewMode::Week => {
// Calculate the start of the week (Sunday) // Calculate the start of the week (Sunday)
let _weekday = date.weekday().num_days_from_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 start = Utc.from_utc_datetime(&start_date.and_hms_opt(0, 0, 0).unwrap());
let end = start + chrono::Duration::days(7); let end = start + chrono::Duration::days(7);
(start, end) (start, end)
}, }
CalendarViewMode::Day => { CalendarViewMode::Day => {
let start = Utc.with_ymd_and_hms(date.year(), date.month(), date.day(), 0, 0, 0).unwrap(); let start = Utc
let end = Utc.with_ymd_and_hms(date.year(), date.month(), date.day(), 23, 59, 59).unwrap(); .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) (start, end)
}, }
}; };
// Get events from Redis // Get events from database
let events = match RedisCalendarService::get_events_in_range(start_date, end_date) { let events = match get_events() {
Ok(events) => 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) => { Err(e) => {
log::error!("Failed to get events from Redis: {}", e); log::error!("Failed to get events from database: {}", e);
vec![] vec![]
} }
}; };
@ -94,7 +159,8 @@ impl CalendarController {
// Generate calendar data based on the view mode // Generate calendar data based on the view mode
match view_mode { match view_mode {
CalendarViewMode::Year => { CalendarViewMode::Year => {
let months = (1..=12).map(|month| { let months = (1..=12)
.map(|month| {
let month_name = match month { let month_name = match month {
1 => "January", 1 => "January",
2 => "February", 2 => "February",
@ -111,7 +177,8 @@ impl CalendarController {
_ => "", _ => "",
}; };
let month_events = events.iter() let month_events = events
.iter()
.filter(|event| { .filter(|event| {
event.start_time.month() == month || event.end_time.month() == month event.start_time.month() == month || event.end_time.month() == month
}) })
@ -123,13 +190,16 @@ impl CalendarController {
name: month_name.to_string(), name: month_name.to_string(),
events: month_events, events: month_events,
} }
}).collect::<Vec<_>>(); })
.collect::<Vec<_>>();
ctx.insert("months", &months); ctx.insert("months", &months);
}, }
CalendarViewMode::Month => { CalendarViewMode::Month => {
let days_in_month = Self::last_day_of_month(date.year(), date.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 first_weekday = first_day.weekday().num_days_from_sunday();
let mut calendar_days = Vec::new(); let mut calendar_days = Vec::new();
@ -145,13 +215,20 @@ impl CalendarController {
// Add days for the current month // Add days for the current month
for day in 1..=days_in_month { for day in 1..=days_in_month {
let day_events = events.iter() let day_events = events
.iter()
.filter(|event| { .filter(|event| {
let day_start = Utc.with_ymd_and_hms(date.year(), date.month(), day, 0, 0, 0).unwrap(); let day_start = Utc
let day_end = Utc.with_ymd_and_hms(date.year(), date.month(), day, 23, 59, 59).unwrap(); .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.start_time <= day_end && event.end_time >= day_start)
(event.all_day && event.start_time.day() <= day && event.end_time.day() >= day) || (event.all_day
&& event.start_time.day() <= day
&& event.end_time.day() >= day)
}) })
.cloned() .cloned()
.collect::<Vec<_>>(); .collect::<Vec<_>>();
@ -175,7 +252,7 @@ impl CalendarController {
ctx.insert("calendar_days", &calendar_days); ctx.insert("calendar_days", &calendar_days);
ctx.insert("month_name", &Self::month_name(date.month())); ctx.insert("month_name", &Self::month_name(date.month()));
}, }
CalendarViewMode::Week => { CalendarViewMode::Week => {
// Calculate the start of the week (Sunday) // Calculate the start of the week (Sunday)
let weekday = date.weekday().num_days_from_sunday(); let weekday = date.weekday().num_days_from_sunday();
@ -184,13 +261,34 @@ impl CalendarController {
let mut week_days = Vec::new(); let mut week_days = Vec::new();
for i in 0..7 { for i in 0..7 {
let day_date = week_start + chrono::Duration::days(i); let day_date = week_start + chrono::Duration::days(i);
let day_events = events.iter() let day_events = events
.iter()
.filter(|event| { .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_start = Utc
let day_end = Utc.with_ymd_and_hms(day_date.year(), day_date.month(), day_date.day(), 23, 59, 59).unwrap(); .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.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()) || (event.all_day
&& event.start_time.day() <= day_date.day()
&& event.end_time.day() >= day_date.day())
}) })
.cloned() .cloned()
.collect::<Vec<_>>(); .collect::<Vec<_>>();
@ -203,16 +301,22 @@ impl CalendarController {
} }
ctx.insert("week_days", &week_days); ctx.insert("week_days", &week_days);
}, }
CalendarViewMode::Day => { CalendarViewMode::Day => {
log::info!("Day view selected"); 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 // Add debug info
log::info!("Events count: {}", events.len()); log::info!("Events count: {}", events.len());
log::info!("Current date: {}", date.format("%Y-%m-%d")); 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) render_template(&tmpl, "calendar/index.html", &ctx)
@ -223,9 +327,24 @@ impl CalendarController {
let mut ctx = tera::Context::new(); let mut ctx = tera::Context::new();
ctx.insert("active_page", "calendar"); 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) { if let Some(user) = Self::get_user_from_session(&_session) {
ctx.insert("user", &user); 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) render_template(&tmpl, "calendar/new_event.html", &ctx)
@ -237,44 +356,91 @@ impl CalendarController {
tmpl: web::Data<Tera>, tmpl: web::Data<Tera>,
_session: Session, _session: Session,
) -> Result<impl Responder> { ) -> 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 // Parse the start and end times
let start_time = match DateTime::parse_from_rfc3339(&form.start_time) { let start_time = match DateTime::parse_from_rfc3339(&form.start_time) {
Ok(dt) => dt.with_timezone(&Utc), Ok(dt) => dt.with_timezone(&Utc),
Err(e) => { Err(e) => {
log::error!("Failed to parse start time: {}", e); log::error!("Failed to parse start time '{}': {}", form.start_time, e);
return Ok(HttpResponse::BadRequest().body("Invalid start time")); return Ok(HttpResponse::BadRequest().body("Invalid start time format"));
} }
}; };
let end_time = match DateTime::parse_from_rfc3339(&form.end_time) { let end_time = match DateTime::parse_from_rfc3339(&form.end_time) {
Ok(dt) => dt.with_timezone(&Utc), Ok(dt) => dt.with_timezone(&Utc),
Err(e) => { Err(e) => {
log::error!("Failed to parse end time: {}", e); log::error!("Failed to parse end time '{}': {}", form.end_time, e);
return Ok(HttpResponse::BadRequest().body("Invalid end time")); return Ok(HttpResponse::BadRequest().body("Invalid end time format"));
} }
}; };
// Create the event // Get user information from session
let event = CalendarEvent::new( let user_info = Self::get_user_from_session(&_session);
form.title.clone(), let (user_id, user_name) = if let Some(user) = &user_info {
form.description.clone(), 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, start_time,
end_time, end_time,
Some(form.color.clone()), None, // location
Some(&form.color),
form.all_day, form.all_day,
None, // User ID would come from session in a real app user_id,
); None, // category
None, // reminder_minutes
) {
Ok((event_id, _saved_event)) => {
log::info!("Created event with ID: {}", event_id);
// Save the event to Redis // If user is logged in, add the event to their calendar
match RedisCalendarService::save_event(&event) { 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(_) => { 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 // Redirect to the calendar page
Ok(HttpResponse::SeeOther() Ok(HttpResponse::SeeOther()
.append_header(("Location", "/calendar")) .append_header(("Location", "/calendar"))
.finish()) .finish())
}, }
Err(e) => { Err(e) => {
log::error!("Failed to save event to Redis: {}", e); log::error!("Failed to save event to database: {}", e);
// Show an error message // Show an error message
let mut ctx = tera::Context::new(); let mut ctx = tera::Context::new();
@ -282,13 +448,15 @@ impl CalendarController {
ctx.insert("error", "Failed to save event"); ctx.insert("error", "Failed to save event");
// Add user to context if available // 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); ctx.insert("user", &user);
} }
let result = render_template(&tmpl, "calendar/new_event.html", &ctx)?; 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()))
} }
} }
} }
@ -300,16 +468,26 @@ impl CalendarController {
) -> Result<impl Responder> { ) -> Result<impl Responder> {
let id = path.into_inner(); let id = path.into_inner();
// Delete the event from Redis // Parse the event ID
match RedisCalendarService::delete_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(_) => { Ok(_) => {
log::info!("Deleted event with ID: {}", event_id);
// Redirect to the calendar page // Redirect to the calendar page
Ok(HttpResponse::SeeOther() Ok(HttpResponse::SeeOther()
.append_header(("Location", "/calendar")) .append_header(("Location", "/calendar"))
.finish()) .finish())
}, }
Err(e) => { 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")) Ok(HttpResponse::InternalServerError().body("Failed to delete event"))
} }
} }
@ -326,7 +504,7 @@ impl CalendarController {
} else { } else {
28 28
} }
}, }
_ => 30, // Default to 30 days _ => 30, // Default to 30 days
} }
} }
@ -387,7 +565,7 @@ pub struct EventForm {
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
struct CalendarDay { struct CalendarDay {
day: u32, day: u32,
events: Vec<CalendarEvent>, events: Vec<Event>,
is_current_month: bool, is_current_month: bool,
} }
@ -396,5 +574,5 @@ struct CalendarDay {
struct CalendarMonth { struct CalendarMonth {
month: u32, month: u32,
name: String, name: String,
events: Vec<CalendarEvent>, events: Vec<Event>,
} }

View File

@ -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 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 // Form structs for company operations
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub struct CompanyRegistrationForm { pub struct CompanyRegistrationForm {
pub company_name: String, pub company_name: String,
pub company_type: String, pub company_type: String,
@ -32,7 +32,9 @@ impl CompanyController {
// Check for success message // Check for success message
if let Some(pos) = query_string.find("success=") { if let Some(pos) = query_string.find("success=") {
let start = pos + 8; // length of "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 success = &query_string[start..end];
let decoded = urlencoding::decode(success).unwrap_or_else(|_| success.into()); let decoded = urlencoding::decode(success).unwrap_or_else(|_| success.into());
context.insert("success", &decoded); context.insert("success", &decoded);
@ -41,16 +43,21 @@ impl CompanyController {
// Check for entity context // Check for entity context
if let Some(pos) = query_string.find("entity=") { if let Some(pos) = query_string.find("entity=") {
let start = pos + 7; // length of "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]; let entity = &query_string[start..end];
context.insert("entity", &entity); context.insert("entity", &entity);
// Also get entity name if present // Also get entity name if present
if let Some(pos) = query_string.find("entity_name=") { if let Some(pos) = query_string.find("entity_name=") {
let start = pos + 12; // length of "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 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); context.insert("entity_name", &decoded_name);
println!("DEBUG: Entity context set to {} ({})", entity, decoded_name); println!("DEBUG: Entity context set to {} ({})", entity, decoded_name);
} }
@ -63,7 +70,10 @@ impl CompanyController {
} }
// View company details // 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 company_id = path.into_inner();
let mut context = Context::new(); let mut context = Context::new();
@ -87,10 +97,7 @@ impl CompanyController {
context.insert("payment_method", &"Credit Card (****4582)"); context.insert("payment_method", &"Credit Card (****4582)");
// Shareholders data // Shareholders data
let shareholders = vec![ let shareholders = vec![("John Smith", "60%"), ("Sarah Johnson", "40%")];
("John Smith", "60%"),
("Sarah Johnson", "40%"),
];
context.insert("shareholders", &shareholders); context.insert("shareholders", &shareholders);
// Contracts data // Contracts data
@ -100,7 +107,7 @@ impl CompanyController {
("Digital Asset Issuance", "Signed"), ("Digital Asset Issuance", "Signed"),
]; ];
context.insert("contracts", &contracts); context.insert("contracts", &contracts);
}, }
"company2" => { "company2" => {
context.insert("company_name", &"Blockchain Innovations Ltd"); context.insert("company_name", &"Blockchain Innovations Ltd");
context.insert("company_type", &"Growth FZC"); context.insert("company_type", &"Growth FZC");
@ -127,7 +134,7 @@ impl CompanyController {
("Physical Asset Holding", "Signed"), ("Physical Asset Holding", "Signed"),
]; ];
context.insert("contracts", &contracts); context.insert("contracts", &contracts);
}, }
"company3" => { "company3" => {
context.insert("company_name", &"Sustainable Energy Cooperative"); context.insert("company_name", &"Sustainable Energy Cooperative");
context.insert("company_type", &"Cooperative FZC"); context.insert("company_type", &"Cooperative FZC");
@ -153,7 +160,7 @@ impl CompanyController {
("Cooperative Governance", "Pending"), ("Cooperative Governance", "Pending"),
]; ];
context.insert("contracts", &contracts); context.insert("contracts", &contracts);
}, }
_ => { _ => {
// If company_id is not recognized, redirect to company index // If company_id is not recognized, redirect to company index
return Ok(HttpResponse::Found() return Ok(HttpResponse::Found()
@ -179,7 +186,7 @@ impl CompanyController {
"company1" => "Zanzibar Digital Solutions", "company1" => "Zanzibar Digital Solutions",
"company2" => "Blockchain Innovations Ltd", "company2" => "Blockchain Innovations Ltd",
"company3" => "Sustainable Energy Cooperative", "company3" => "Sustainable Energy Cooperative",
_ => "Unknown Company" _ => "Unknown Company",
}; };
// In a real application, we would set a session/cookie for the current entity // In a real application, we would set a session/cookie for the current entity
@ -188,16 +195,21 @@ impl CompanyController {
let encoded_message = urlencoding::encode(&success_message); let encoded_message = urlencoding::encode(&success_message);
Ok(HttpResponse::Found() Ok(HttpResponse::Found()
.append_header(("Location", format!("/company?success={}&entity={}&entity_name={}", .append_header((
encoded_message, company_id, urlencoding::encode(company_name)))) "Location",
format!(
"/company?success={}&entity={}&entity_name={}",
encoded_message,
company_id,
urlencoding::encode(company_name)
),
))
.finish()) .finish())
} }
// Process company registration // Process company registration
pub async fn register( pub async fn register(mut form: actix_multipart::Multipart) -> Result<HttpResponse> {
mut form: actix_multipart::Multipart, use actix_web::http::header;
) -> Result<HttpResponse> {
use actix_web::{http::header};
use futures_util::stream::StreamExt as _; use futures_util::stream::StreamExt as _;
use std::collections::HashMap; use std::collections::HashMap;
@ -220,7 +232,10 @@ impl CompanyController {
if name == "company_docs" { if name == "company_docs" {
files.push(value); // Just collect files in memory for now files.push(value); // Just collect files in memory for now
} else { } 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(),
);
} }
} }
} }
@ -231,15 +246,26 @@ impl CompanyController {
let shareholders = fields.get("shareholders").cloned().unwrap_or_default(); let shareholders = fields.get("shareholders").cloned().unwrap_or_default();
// Log received fields (mock DB insert) // Log received fields (mock DB insert)
println!("[Company Registration] Name: {}, Type: {}, Shareholders: {}, Files: {}", println!(
company_name, company_type, shareholders, files.len()); "[Company Registration] Name: {}, Type: {}, Shareholders: {}, Files: {}",
company_name,
company_type,
shareholders,
files.len()
);
// Create success message // 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 // Redirect back to /company with success message
Ok(HttpResponse::SeeOther() 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()) .finish())
} }
} }

View File

@ -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::web::Query;
use actix_web::{Error, HttpResponse, Result, web};
use chrono::{Duration, Utc};
use serde::Deserialize;
use std::collections::HashMap; 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; use crate::utils::render_template;
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub struct ContractForm { pub struct ContractForm {
pub title: String, pub title: String,
pub description: String, pub description: String,
@ -18,6 +21,7 @@ pub struct ContractForm {
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub struct SignerForm { pub struct SignerForm {
pub name: String, pub name: String,
pub email: String, pub email: String,
@ -49,7 +53,8 @@ impl ContractController {
context.insert("recent_contracts", &recent_contracts); context.insert("recent_contracts", &recent_contracts);
// Add pending signature contracts // Add pending signature contracts
let pending_signature_contracts: Vec<serde_json::Map<String, serde_json::Value>> = contracts let pending_signature_contracts: Vec<serde_json::Map<String, serde_json::Value>> =
contracts
.iter() .iter()
.filter(|c| c.status == ContractStatus::PendingSignatures) .filter(|c| c.status == ContractStatus::PendingSignatures)
.map(|c| Self::contract_to_json(c)) .map(|c| Self::contract_to_json(c))
@ -110,7 +115,7 @@ impl ContractController {
pub async fn detail( pub async fn detail(
tmpl: web::Data<Tera>, tmpl: web::Data<Tera>,
path: web::Path<String>, path: web::Path<String>,
query: Query<HashMap<String, String>> query: Query<HashMap<String, String>>,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, Error> {
let contract_id = path.into_inner(); let contract_id = path.into_inner();
let mut context = Context::new(); let mut context = Context::new();
@ -137,10 +142,13 @@ impl ContractController {
context.insert("contract", &contract_json); context.insert("contract", &contract_json);
// If this contract uses multi-page markdown, load the selected section // 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) { if let (Some(content_dir), Some(toc)) = (&contract.content_dir, &contract.toc) {
use pulldown_cmark::{Options, Parser, html};
use std::fs; use std::fs;
use pulldown_cmark::{Parser, Options, html};
// Helper to flatten toc recursively // Helper to flatten toc recursively
fn flatten_toc<'a>(items: &'a Vec<TocItem>, out: &mut Vec<&'a TocItem>) { fn flatten_toc<'a>(items: &'a Vec<TocItem>, out: &mut Vec<&'a TocItem>) {
for item in items { for item in items {
@ -154,15 +162,28 @@ impl ContractController {
flatten_toc(&toc, &mut flat_toc); flatten_toc(&toc, &mut flat_toc);
let section_param = query.get("section"); let section_param = query.get("section");
let selected_file = section_param let selected_file = section_param
.and_then(|f| flat_toc.iter().find(|item| item.file == *f).map(|item| item.file.clone())) .and_then(|f| {
.unwrap_or_else(|| flat_toc.get(0).map(|item| item.file.clone()).unwrap_or_default()); 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); context.insert("section", &selected_file);
let rel_path = format!("{}/{}", content_dir, selected_file); let rel_path = format!("{}/{}", content_dir, selected_file);
let abs_path = match std::env::current_dir() { let abs_path = match std::env::current_dir() {
Ok(dir) => dir.join(&rel_path), Ok(dir) => dir.join(&rel_path),
Err(_) => std::path::PathBuf::from(&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) { match fs::read_to_string(&abs_path) {
Ok(md) => { Ok(md) => {
println!("DEBUG: Successfully read markdown file"); println!("DEBUG: Successfully read markdown file");
@ -170,9 +191,12 @@ impl ContractController {
let mut html_output = String::new(); let mut html_output = String::new();
html::push_html(&mut html_output, parser); html::push_html(&mut html_output, parser);
context.insert("contract_section_content", &html_output); context.insert("contract_section_content", &html_output);
}, }
Err(e) => { 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); println!("{}", error_msg);
context.insert("contract_section_content_error", &error_msg); context.insert("contract_section_content_error", &error_msg);
} }
@ -181,11 +205,19 @@ impl ContractController {
} }
// Count signed signers for the template // 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); context.insert("signed_signers", &signed_signers);
// Count pending signers for the template // 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); context.insert("pending_signers", &pending_signers);
// For demo purposes, set user_has_signed to false // For demo purposes, set user_has_signed to false
@ -208,7 +240,7 @@ impl ContractController {
("Employment", "Employment Contract"), ("Employment", "Employment Contract"),
("NDA", "Non-Disclosure Agreement"), ("NDA", "Non-Disclosure Agreement"),
("SLA", "Service Level Agreement"), ("SLA", "Service Level Agreement"),
("Other", "Other") ("Other", "Other"),
]; ];
context.insert("contract_types", &contract_types); context.insert("contract_types", &contract_types);
@ -224,7 +256,9 @@ impl ContractController {
// In a real application, we would save the contract to the database // In a real application, we would save the contract to the database
// For now, we'll just redirect to the contracts list // 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 // Helper method to convert Contract to a JSON object for templates
@ -232,46 +266,99 @@ impl ContractController {
let mut map = serde_json::Map::new(); let mut map = serde_json::Map::new();
// Basic contract info // Basic contract info
map.insert("id".to_string(), serde_json::Value::String(contract.id.clone())); map.insert(
map.insert("title".to_string(), serde_json::Value::String(contract.title.clone())); "id".to_string(),
map.insert("description".to_string(), serde_json::Value::String(contract.description.clone())); serde_json::Value::String(contract.id.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(
map.insert("created_by".to_string(), serde_json::Value::String(contract.created_by.clone())); "title".to_string(),
map.insert("created_at".to_string(), serde_json::Value::String(contract.created_at.format("%Y-%m-%d").to_string())); serde_json::Value::String(contract.title.clone()),
map.insert("updated_at".to_string(), serde_json::Value::String(contract.updated_at.format("%Y-%m-%d").to_string())); );
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 // Organization info
if let Some(org) = &contract.organization_id { 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 { } else {
map.insert("organization".to_string(), serde_json::Value::Null); map.insert("organization".to_string(), serde_json::Value::Null);
} }
// Add signers // Add signers
let signers: Vec<serde_json::Value> = contract.signers.iter() let signers: Vec<serde_json::Value> = contract
.signers
.iter()
.map(|s| { .map(|s| {
let mut signer_map = serde_json::Map::new(); let mut signer_map = serde_json::Map::new();
signer_map.insert("id".to_string(), serde_json::Value::String(s.id.clone())); 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(
signer_map.insert("email".to_string(), serde_json::Value::String(s.email.clone())); "name".to_string(),
signer_map.insert("status".to_string(), serde_json::Value::String(s.status.as_str().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 { 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 { } else {
// For display purposes, add a placeholder date for pending signers // For display purposes, add a placeholder date for pending signers
if s.status == SignerStatus::Pending { 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 { } 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 { 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 { } 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) serde_json::Value::Object(signer_map)
@ -281,91 +368,212 @@ impl ContractController {
map.insert("signers".to_string(), serde_json::Value::Array(signers)); map.insert("signers".to_string(), serde_json::Value::Array(signers));
// Add pending_signers count for templates // Add pending_signers count for templates
let pending_signers = contract.signers.iter().filter(|s| s.status == SignerStatus::Pending).count(); let pending_signers = contract
map.insert("pending_signers".to_string(), serde_json::Value::Number(serde_json::Number::from(pending_signers))); .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 // Add signed_signers count for templates
let signed_signers = contract.signers.iter().filter(|s| s.status == SignerStatus::Signed).count(); let signed_signers = contract
map.insert("signed_signers".to_string(), serde_json::Value::Number(serde_json::Number::from(signed_signers))); .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 // Add revisions
let revisions: Vec<serde_json::Value> = contract.revisions.iter() let revisions: Vec<serde_json::Value> = contract
.revisions
.iter()
.map(|r| { .map(|r| {
let mut revision_map = serde_json::Map::new(); 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(
revision_map.insert("content".to_string(), serde_json::Value::String(r.content.clone())); "version".to_string(),
revision_map.insert("created_at".to_string(), serde_json::Value::String(r.created_at.format("%Y-%m-%d").to_string())); serde_json::Value::Number(serde_json::Number::from(r.version)),
revision_map.insert("created_by".to_string(), serde_json::Value::String(r.created_by.clone())); );
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 { 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 // 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 { } else {
revision_map.insert("comments".to_string(), serde_json::Value::String("".to_string())); revision_map.insert(
revision_map.insert("notes".to_string(), serde_json::Value::String("".to_string())); "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) serde_json::Value::Object(revision_map)
}) })
.collect(); .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 // 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 // Add latest_revision as an object
if !contract.revisions.is_empty() { if !contract.revisions.is_empty() {
// Find the latest revision based on version number // Find the latest revision based on version number
if let Some(latest) = contract.revisions.iter().max_by_key(|r| r.version) { if let Some(latest) = contract.revisions.iter().max_by_key(|r| r.version) {
let mut latest_revision_map = serde_json::Map::new(); 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(
latest_revision_map.insert("content".to_string(), serde_json::Value::String(latest.content.clone())); "version".to_string(),
latest_revision_map.insert("created_at".to_string(), serde_json::Value::String(latest.created_at.format("%Y-%m-%d").to_string())); serde_json::Value::Number(serde_json::Number::from(latest.version)),
latest_revision_map.insert("created_by".to_string(), serde_json::Value::String(latest.created_by.clone())); );
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 { if let Some(comments) = &latest.comments {
latest_revision_map.insert("comments".to_string(), serde_json::Value::String(comments.clone())); latest_revision_map.insert(
latest_revision_map.insert("notes".to_string(), serde_json::Value::String(comments.clone())); "comments".to_string(),
serde_json::Value::String(comments.clone()),
);
latest_revision_map.insert(
"notes".to_string(),
serde_json::Value::String(comments.clone()),
);
} else { } else {
latest_revision_map.insert("comments".to_string(), serde_json::Value::String("".to_string())); latest_revision_map.insert(
latest_revision_map.insert("notes".to_string(), serde_json::Value::String("".to_string())); "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 { } else {
// Create an empty latest_revision object to avoid template errors // Create an empty latest_revision object to avoid template errors
let mut empty_revision = serde_json::Map::new(); 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(
empty_revision.insert("content".to_string(), serde_json::Value::String("No content available".to_string())); "version".to_string(),
empty_revision.insert("created_at".to_string(), serde_json::Value::String("N/A".to_string())); serde_json::Value::Number(serde_json::Number::from(0)),
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(
empty_revision.insert("notes".to_string(), serde_json::Value::String("".to_string())); "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)); map.insert(
"latest_revision".to_string(),
serde_json::Value::Object(empty_revision),
);
} }
} else { } else {
// Create an empty latest_revision object to avoid template errors // Create an empty latest_revision object to avoid template errors
let mut empty_revision = serde_json::Map::new(); 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(
empty_revision.insert("content".to_string(), serde_json::Value::String("No content available".to_string())); "version".to_string(),
empty_revision.insert("created_at".to_string(), serde_json::Value::String("N/A".to_string())); serde_json::Value::Number(serde_json::Number::from(0)),
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(
empty_revision.insert("notes".to_string(), serde_json::Value::String("".to_string())); "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)); map.insert(
"latest_revision".to_string(),
serde_json::Value::Object(empty_revision),
);
} }
// Add effective and expiration dates if present // Add effective and expiration dates if present
if let Some(effective_date) = &contract.effective_date { 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 { 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 map
@ -554,7 +762,6 @@ impl ContractController {
]), ]),
}; };
// Add potential signers to contract 3 (still in draft) // Add potential signers to contract 3 (still in draft)
contract3.signers.push(ContractSigner { contract3.signers.push(ContractSigner {
id: "signer-006".to_string(), id: "signer-006".to_string(),
@ -576,8 +783,7 @@ impl ContractController {
// Add ToC and content directory to contract 3 // Add ToC and content directory to contract 3
contract3.content_dir = Some("src/content/contract-003".to_string()); contract3.content_dir = Some("src/content/contract-003".to_string());
contract3.toc = Some(vec![ contract3.toc = Some(vec![TocItem {
TocItem {
title: "Digital Asset Tokenization Agreement".to_string(), title: "Digital Asset Tokenization Agreement".to_string(),
file: "cover.md".to_string(), file: "cover.md".to_string(),
children: vec![ children: vec![
@ -622,8 +828,7 @@ impl ContractController {
children: vec![], children: vec![],
}, },
], ],
} }]);
]);
// No revision content for contract 3, content is in markdown files. // No revision content for contract 3, content is in markdown files.
// Mock contract 4 - Rejected // Mock contract 4 - Rejected

View File

@ -1,12 +1,15 @@
use actix_web::{web, HttpResponse, Result};
use actix_web::HttpRequest; use actix_web::HttpRequest;
use tera::{Context, Tera}; use actix_web::{HttpResponse, Result, web};
use chrono::{Utc, Duration}; use chrono::{Duration, Utc};
use serde::Deserialize; use serde::Deserialize;
use tera::{Context, Tera};
use uuid::Uuid; use uuid::Uuid;
use crate::models::asset::{Asset, AssetType, AssetStatus}; use crate::models::asset::Asset;
use crate::models::defi::{DefiPosition, DefiPositionType, DefiPositionStatus, ProvidingPosition, ReceivingPosition, DEFI_DB}; use crate::models::defi::{
DEFI_DB, DefiPosition, DefiPositionStatus, DefiPositionType, ProvidingPosition,
ReceivingPosition,
};
use crate::utils::render_template; use crate::utils::render_template;
// Form structs for DeFi operations // Form structs for DeFi operations
@ -26,6 +29,7 @@ pub struct ReceivingForm {
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub struct LiquidityForm { pub struct LiquidityForm {
pub first_token: String, pub first_token: String,
pub first_amount: f64, pub first_amount: f64,
@ -35,6 +39,7 @@ pub struct LiquidityForm {
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub struct StakingForm { pub struct StakingForm {
pub asset_id: String, pub asset_id: String,
pub amount: f64, pub amount: f64,
@ -49,6 +54,7 @@ pub struct SwapForm {
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub struct CollateralForm { pub struct CollateralForm {
pub asset_id: String, pub asset_id: String,
pub amount: f64, pub amount: f64,
@ -116,7 +122,10 @@ impl DefiController {
} }
// Process providing request // 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); println!("DEBUG: Processing providing request: {:?}", form);
// Get the asset obligationails (in a real app, this would come from a database) // Get the asset obligationails (in a real app, this would come from a database)
@ -134,7 +143,8 @@ impl DefiController {
_ => 4.2, // Default to 30 days rate _ => 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 // Create a new providing position
let providing_position = ProvidingPosition { let providing_position = ProvidingPosition {
@ -164,9 +174,15 @@ impl DefiController {
} }
// Redirect with success message // 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() Ok(HttpResponse::SeeOther()
.append_header(("Location", format!("/defi?success={}", urlencoding::encode(&success_message)))) .append_header((
"Location",
format!("/defi?success={}", urlencoding::encode(&success_message)),
))
.finish()) .finish())
} else { } else {
// Asset not found, redirect with error // Asset not found, redirect with error
@ -177,7 +193,10 @@ impl DefiController {
} }
// Process receiving request // 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); println!("DEBUG: Processing receiving request: {:?}", form);
// Get the asset obligationails (in a real app, this would come from a database) // Get the asset obligationails (in a real app, this would come from a database)
@ -196,11 +215,13 @@ impl DefiController {
}; };
// Calculate profit share and total to repay // 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; let total_to_repay = form.amount + profit_share;
// Calculate collateral value and ratio // 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; let collateral_ratio = (collateral_value / form.amount) * 100.0;
// Create a new receiving position // Create a new receiving position
@ -238,10 +259,15 @@ impl DefiController {
} }
// Redirect with success message // Redirect with success message
let success_message = format!("Successfully borrowed {} ZDFZ using {} {} as collateral", let success_message = format!(
form.amount, form.collateral_amount, collateral_asset.name); "Successfully borrowed {} ZDFZ using {} {} as collateral",
form.amount, form.collateral_amount, collateral_asset.name
);
Ok(HttpResponse::SeeOther() Ok(HttpResponse::SeeOther()
.append_header(("Location", format!("/defi?success={}", urlencoding::encode(&success_message)))) .append_header((
"Location",
format!("/defi?success={}", urlencoding::encode(&success_message)),
))
.finish()) .finish())
} else { } else {
// Asset not found, redirect with error // Asset not found, redirect with error
@ -252,22 +278,33 @@ impl DefiController {
} }
// Process liquidity provision // 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); println!("DEBUG: Processing liquidity provision: {:?}", form);
// In a real application, this would add liquidity to a pool in the database // 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 // For now, we'll just redirect back to the DeFi dashboard with a success message
let success_message = format!("Successfully added liquidity: {} {} and {} {}", let success_message = format!(
form.first_amount, form.first_token, form.second_amount, form.second_token); "Successfully added liquidity: {} {} and {} {}",
form.first_amount, form.first_token, form.second_amount, form.second_token
);
Ok(HttpResponse::SeeOther() Ok(HttpResponse::SeeOther()
.append_header(("Location", format!("/defi?success={}", urlencoding::encode(&success_message)))) .append_header((
"Location",
format!("/defi?success={}", urlencoding::encode(&success_message)),
))
.finish()) .finish())
} }
// Process staking request // 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); println!("DEBUG: Processing staking request: {:?}", form);
// In a real application, this would create a staking position in the database // In a real application, this would create a staking position in the database
@ -276,27 +313,41 @@ impl DefiController {
let success_message = format!("Successfully staked {} {}", form.amount, form.asset_id); let success_message = format!("Successfully staked {} {}", form.amount, form.asset_id);
Ok(HttpResponse::SeeOther() Ok(HttpResponse::SeeOther()
.append_header(("Location", format!("/defi?success={}", urlencoding::encode(&success_message)))) .append_header((
"Location",
format!("/defi?success={}", urlencoding::encode(&success_message)),
))
.finish()) .finish())
} }
// Process token swap // 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); println!("DEBUG: Processing token swap: {:?}", form);
// In a real application, this would perform a token swap in the database // 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 // For now, we'll just redirect back to the DeFi dashboard with a success message
let success_message = format!("Successfully swapped {} {} to {}", let success_message = format!(
form.from_amount, form.from_token, form.to_token); "Successfully swapped {} {} to {}",
form.from_amount, form.from_token, form.to_token
);
Ok(HttpResponse::SeeOther() Ok(HttpResponse::SeeOther()
.append_header(("Location", format!("/defi?success={}", urlencoding::encode(&success_message)))) .append_header((
"Location",
format!("/defi?success={}", urlencoding::encode(&success_message)),
))
.finish()) .finish())
} }
// Process collateral position creation // 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); println!("DEBUG: Processing collateral creation: {:?}", form);
// In a real application, this would create a collateral position in the database // In a real application, this would create a collateral position in the database
@ -309,11 +360,16 @@ impl DefiController {
_ => "collateralization", _ => "collateralization",
}; };
let success_message = format!("Successfully collateralized {} {} for {}", let success_message = format!(
form.amount, form.asset_id, purpose_str); "Successfully collateralized {} {} for {}",
form.amount, form.asset_id, purpose_str
);
Ok(HttpResponse::SeeOther() Ok(HttpResponse::SeeOther()
.append_header(("Location", format!("/defi?success={}", urlencoding::encode(&success_message)))) .append_header((
"Location",
format!("/defi?success={}", urlencoding::encode(&success_message)),
))
.finish()) .finish())
} }
@ -322,12 +378,32 @@ impl DefiController {
let mut stats = serde_json::Map::new(); let mut stats = serde_json::Map::new();
// Handle Option<Number> by unwrapping with expect // 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(
stats.insert("providing_volume".to_string(), serde_json::Value::Number(serde_json::Number::from_f64(450000.0).expect("Valid float"))); "total_value_locked".to_string(),
stats.insert("receiving_volume".to_string(), serde_json::Value::Number(serde_json::Number::from_f64(320000.0).expect("Valid float"))); serde_json::Value::Number(
stats.insert("liquidity_pools_count".to_string(), serde_json::Value::Number(serde_json::Number::from(12))); serde_json::Number::from_f64(1250000.0).expect("Valid float"),
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(
"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 stats
} }
@ -336,25 +412,61 @@ impl DefiController {
fn asset_to_json(asset: &Asset) -> serde_json::Map<String, serde_json::Value> { fn asset_to_json(asset: &Asset) -> serde_json::Map<String, serde_json::Value> {
let mut map = serde_json::Map::new(); let mut map = serde_json::Map::new();
map.insert("id".to_string(), serde_json::Value::String(asset.id.clone())); map.insert(
map.insert("name".to_string(), serde_json::Value::String(asset.name.clone())); "id".to_string(),
map.insert("description".to_string(), serde_json::Value::String(asset.description.clone())); serde_json::Value::String(asset.id.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(
"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 // Add current valuation
if let Some(latest) = asset.latest_valuation() { if let Some(latest) = asset.latest_valuation() {
if let Some(num) = serde_json::Number::from_f64(latest.value) { 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 { } 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(
map.insert("valuation_date".to_string(), serde_json::Value::String(latest.date.format("%Y-%m-%d").to_string())); "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 { } else {
map.insert("current_valuation".to_string(), serde_json::Value::Number(serde_json::Number::from(0))); map.insert(
map.insert("valuation_currency".to_string(), serde_json::Value::String("USD".to_string())); "current_valuation".to_string(),
map.insert("valuation_date".to_string(), serde_json::Value::String("N/A".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 map

View File

@ -609,6 +609,7 @@ impl FlowController {
/// Form for creating a new flow /// Form for creating a new flow
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub struct FlowForm { pub struct FlowForm {
/// Flow name /// Flow name
pub name: String, pub name: String,
@ -620,6 +621,7 @@ pub struct FlowForm {
/// Form for marking a step as stuck /// Form for marking a step as stuck
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub struct StuckForm { pub struct StuckForm {
/// Reason for being stuck /// Reason for being stuck
pub reason: String, pub reason: String,
@ -627,6 +629,7 @@ pub struct StuckForm {
/// Form for adding a log to a step /// Form for adding a log to a step
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub struct LogForm { pub struct LogForm {
/// Log message /// Log message
pub message: String, pub message: String,

File diff suppressed because it is too large Load Diff

View File

@ -96,6 +96,7 @@ impl HomeController {
/// Represents the data submitted in the contact form /// Represents the data submitted in the contact form
#[derive(Debug, serde::Deserialize)] #[derive(Debug, serde::Deserialize)]
#[allow(dead_code)]
pub struct ContactForm { pub struct ContactForm {
pub name: String, pub name: String,
pub email: String, pub email: String,

View File

@ -1,12 +1,11 @@
use actix_web::{web, HttpResponse, Result, http}; use actix_web::{HttpResponse, Result, http, web};
use tera::{Context, Tera}; use chrono::{Duration, Utc};
use chrono::{Utc, Duration};
use serde::Deserialize; 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::controllers::asset::AssetController;
use crate::models::asset::{Asset, AssetStatus, AssetType};
use crate::models::marketplace::{Listing, ListingStatus, ListingType, MarketplaceStatistics};
use crate::utils::render_template; use crate::utils::render_template;
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@ -22,6 +21,7 @@ pub struct ListingForm {
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub struct BidForm { pub struct BidForm {
pub amount: f64, pub amount: f64,
pub currency: String, pub currency: String,
@ -43,13 +43,15 @@ impl MarketplaceController {
let stats = MarketplaceStatistics::new(&listings); let stats = MarketplaceStatistics::new(&listings);
// Get featured listings (up to 4) // 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) .filter(|l| l.featured && l.status == ListingStatus::Active)
.take(4) .take(4)
.collect(); .collect();
// Get recent listings (up to 8) // 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) .filter(|l| l.status == ListingStatus::Active)
.collect(); .collect();
@ -58,7 +60,8 @@ impl MarketplaceController {
let recent_listings = recent_listings.into_iter().take(8).collect::<Vec<_>>(); let recent_listings = recent_listings.into_iter().take(8).collect::<Vec<_>>();
// Get recent sales (up to 5) // 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) .filter(|l| l.status == ListingStatus::Sold)
.collect(); .collect();
@ -87,18 +90,24 @@ impl MarketplaceController {
let listings = Self::get_mock_listings(); let listings = Self::get_mock_listings();
// Filter active listings // Filter active listings
let active_listings: Vec<&Listing> = listings.iter() let active_listings: Vec<&Listing> = listings
.iter()
.filter(|l| l.status == ListingStatus::Active) .filter(|l| l.status == ListingStatus::Active)
.collect(); .collect();
context.insert("active_page", &"marketplace"); context.insert("active_page", &"marketplace");
context.insert("listings", &active_listings); context.insert("listings", &active_listings);
context.insert("listing_types", &[ context.insert(
"listing_types",
&[
ListingType::FixedPrice.as_str(), ListingType::FixedPrice.as_str(),
ListingType::Auction.as_str(), ListingType::Auction.as_str(),
ListingType::Exchange.as_str(), ListingType::Exchange.as_str(),
]); ],
context.insert("asset_types", &[ );
context.insert(
"asset_types",
&[
AssetType::Token.as_str(), AssetType::Token.as_str(),
AssetType::Artwork.as_str(), AssetType::Artwork.as_str(),
AssetType::RealEstate.as_str(), AssetType::RealEstate.as_str(),
@ -107,7 +116,8 @@ impl MarketplaceController {
AssetType::Share.as_str(), AssetType::Share.as_str(),
AssetType::Bond.as_str(), AssetType::Bond.as_str(),
AssetType::Other.as_str(), AssetType::Other.as_str(),
]); ],
);
render_template(&tmpl, "marketplace/listings.html", &context) render_template(&tmpl, "marketplace/listings.html", &context)
} }
@ -120,9 +130,8 @@ impl MarketplaceController {
// Filter by current user (mock user ID) // Filter by current user (mock user ID)
let user_id = "user-123"; let user_id = "user-123";
let my_listings: Vec<&Listing> = listings.iter() let my_listings: Vec<&Listing> =
.filter(|l| l.seller_id == user_id) listings.iter().filter(|l| l.seller_id == user_id).collect();
.collect();
context.insert("active_page", &"marketplace"); context.insert("active_page", &"marketplace");
context.insert("listings", &my_listings); context.insert("listings", &my_listings);
@ -131,7 +140,10 @@ impl MarketplaceController {
} }
// Display listing details // 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 listing_id = path.into_inner();
let mut context = Context::new(); let mut context = Context::new();
@ -142,15 +154,19 @@ impl MarketplaceController {
if let Some(listing) = listing { if let Some(listing) = listing {
// Get similar listings (same asset type, active) // Get similar listings (same asset type, active)
let similar_listings: Vec<&Listing> = listings.iter() let similar_listings: Vec<&Listing> = listings
.filter(|l| l.asset_type == listing.asset_type && .iter()
l.status == ListingStatus::Active && .filter(|l| {
l.id != listing.id) l.asset_type == listing.asset_type
&& l.status == ListingStatus::Active
&& l.id != listing.id
})
.take(4) .take(4)
.collect(); .collect();
// Get highest bid amount and minimum bid for auction listings // 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() { if let Some(bid) = listing.highest_bid() {
(Some(bid.amount), bid.amount + 1.0) (Some(bid.amount), bid.amount + 1.0)
} else { } else {
@ -186,17 +202,21 @@ impl MarketplaceController {
let assets = AssetController::get_mock_assets(); let assets = AssetController::get_mock_assets();
let user_id = "user-123"; // Mock user ID 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) .filter(|a| a.owner_id == user_id && a.status == AssetStatus::Active)
.collect(); .collect();
context.insert("active_page", &"marketplace"); context.insert("active_page", &"marketplace");
context.insert("assets", &user_assets); context.insert("assets", &user_assets);
context.insert("listing_types", &[ context.insert(
"listing_types",
&[
ListingType::FixedPrice.as_str(), ListingType::FixedPrice.as_str(),
ListingType::Auction.as_str(), ListingType::Auction.as_str(),
ListingType::Exchange.as_str(), ListingType::Exchange.as_str(),
]); ],
);
render_template(&tmpl, "marketplace/create_listing.html", &context) render_template(&tmpl, "marketplace/create_listing.html", &context)
} }
@ -215,7 +235,8 @@ impl MarketplaceController {
if let Some(asset) = asset { if let Some(asset) = asset {
// Process tags // Process tags
let tags = match form.tags { let tags = match form.tags {
Some(tags_str) => tags_str.split(',') Some(tags_str) => tags_str
.split(',')
.map(|s| s.trim().to_string()) .map(|s| s.trim().to_string())
.filter(|s| !s.is_empty()) .filter(|s| !s.is_empty())
.collect(), .collect(),
@ -223,9 +244,9 @@ impl MarketplaceController {
}; };
// Calculate expiration date if provided // Calculate expiration date if provided
let expires_at = form.duration_days.map(|days| { let expires_at = form
Utc::now() + Duration::days(days as i64) .duration_days
}); .map(|days| Utc::now() + Duration::days(days as i64));
// Parse listing type // Parse listing type
let listing_type = match form.listing_type.as_str() { let listing_type = match form.listing_type.as_str() {
@ -273,13 +294,14 @@ impl MarketplaceController {
} }
// Submit a bid on an auction listing // Submit a bid on an auction listing
#[allow(dead_code)]
pub async fn submit_bid( pub async fn submit_bid(
tmpl: web::Data<Tera>, _tmpl: web::Data<Tera>,
path: web::Path<String>, path: web::Path<String>,
form: web::Form<BidForm>, _form: web::Form<BidForm>,
) -> Result<HttpResponse> { ) -> Result<HttpResponse> {
let listing_id = path.into_inner(); let listing_id = path.into_inner();
let form = form.into_inner(); let _form = _form.into_inner();
// In a real application, we would: // In a real application, we would:
// 1. Find the listing in the database // 1. Find the listing in the database
@ -289,13 +311,16 @@ impl MarketplaceController {
// For now, we'll just redirect back to the listing // For now, we'll just redirect back to the listing
Ok(HttpResponse::SeeOther() Ok(HttpResponse::SeeOther()
.insert_header((http::header::LOCATION, format!("/marketplace/{}", listing_id))) .insert_header((
http::header::LOCATION,
format!("/marketplace/{}", listing_id),
))
.finish()) .finish())
} }
// Purchase a fixed-price listing // Purchase a fixed-price listing
pub async fn purchase_listing( pub async fn purchase_listing(
tmpl: web::Data<Tera>, _tmpl: web::Data<Tera>,
path: web::Path<String>, path: web::Path<String>,
form: web::Form<PurchaseForm>, form: web::Form<PurchaseForm>,
) -> Result<HttpResponse> { ) -> Result<HttpResponse> {
@ -305,7 +330,10 @@ impl MarketplaceController {
if !form.agree_to_terms { if !form.agree_to_terms {
// User must agree to terms // User must agree to terms
return Ok(HttpResponse::SeeOther() return Ok(HttpResponse::SeeOther()
.insert_header((http::header::LOCATION, format!("/marketplace/{}", listing_id))) .insert_header((
http::header::LOCATION,
format!("/marketplace/{}", listing_id),
))
.finish()); .finish());
} }
@ -324,7 +352,7 @@ impl MarketplaceController {
// Cancel a listing // Cancel a listing
pub async fn cancel_listing( pub async fn cancel_listing(
tmpl: web::Data<Tera>, _tmpl: web::Data<Tera>,
path: web::Path<String>, path: web::Path<String>,
) -> Result<HttpResponse> { ) -> Result<HttpResponse> {
let _listing_id = path.into_inner(); let _listing_id = path.into_inner();
@ -368,7 +396,10 @@ impl MarketplaceController {
let mut listing = Listing::new( let mut listing = Listing::new(
format!("{} for Sale", asset.name), 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.id.clone(),
asset.name.clone(), asset.name.clone(),
asset.asset_type.clone(), asset.asset_type.clone(),
@ -427,7 +458,8 @@ impl MarketplaceController {
let num_bids = 2 + (i % 3); let num_bids = 2 + (i % 3);
for j in 0..num_bids { for j in 0..num_bids {
let bidder_index = (j + 1) % user_ids.len(); 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 bid_amount = starting_price * (1.0 + (0.1 * (j + 1) as f64));
let _ = listing.add_bid( let _ = listing.add_bid(
user_ids[bidder_index].to_string(), user_ids[bidder_index].to_string(),
@ -465,7 +497,10 @@ impl MarketplaceController {
let listing = Listing::new( let listing = Listing::new(
format!("Trade: {}", asset.name), 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.id.clone(),
asset.name.clone(), asset.name.clone(),
asset.asset_type.clone(), asset.asset_type.clone(),

View 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)
}

View 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)
}

View 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)
}

View File

@ -0,0 +1,4 @@
pub mod calendar;
pub mod contracts;
pub mod db;
pub mod governance;

View File

@ -1,22 +1,23 @@
use actix_files as fs; use actix_files as fs;
use actix_web::{App, HttpServer, web};
use actix_web::middleware::Logger; use actix_web::middleware::Logger;
use tera::Tera; use actix_web::{App, HttpServer, web};
use std::io;
use std::env;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use std::env;
use std::io;
use tera::Tera;
mod config; mod config;
mod controllers; mod controllers;
mod db;
mod middleware; mod middleware;
mod models; mod models;
mod routes; mod routes;
mod utils; mod utils;
// Import middleware components // Import middleware components
use middleware::{RequestTimer, SecurityHeaders, JwtAuth}; use middleware::{JwtAuth, RequestTimer, SecurityHeaders};
use utils::redis_service;
use models::initialize_mock_data; use models::initialize_mock_data;
use utils::redis_service;
// Initialize lazy_static for in-memory storage // Initialize lazy_static for in-memory storage
extern crate lazy_static; extern crate lazy_static;
@ -65,7 +66,8 @@ async fn main() -> io::Result<()> {
let bind_address = format!("{}:{}", config.server.host, port); let bind_address = format!("{}:{}", config.server.host, port);
// Initialize Redis client // 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) { if let Err(e) = redis_service::init_redis_client(&redis_url) {
log::error!("Failed to initialize Redis client: {}", e); log::error!("Failed to initialize Redis client: {}", e);
log::warn!("Calendar functionality will not work properly without Redis"); log::warn!("Calendar functionality will not work properly without Redis");
@ -77,6 +79,9 @@ async fn main() -> io::Result<()> {
initialize_mock_data(); initialize_mock_data();
log::info!("DeFi mock data initialized successfully"); 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); log::info!("Starting server at http://{}", bind_address);
// Create and configure the HTTP server // Create and configure the HTTP server

View File

@ -112,6 +112,7 @@ pub struct Asset {
pub external_url: Option<String>, pub external_url: Option<String>,
} }
#[allow(dead_code)]
impl Asset { impl Asset {
/// Creates a new asset /// Creates a new asset
pub fn new( pub fn new(

View File

@ -1,61 +1,4 @@
use chrono::{DateTime, Utc}; // No imports needed for this module currently
use serde::{Deserialize, Serialize};
use uuid::Uuid;
/// Represents a calendar event
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CalendarEvent {
/// Unique identifier for the event
pub id: String,
/// Title of the event
pub title: String,
/// Description of the event
pub description: String,
/// Start time of the event
pub start_time: DateTime<Utc>,
/// End time of the event
pub end_time: DateTime<Utc>,
/// Color of the event (hex code)
pub color: String,
/// Whether the event is an all-day event
pub all_day: bool,
/// User ID of the event creator
pub user_id: Option<String>,
}
impl CalendarEvent {
/// Creates a new calendar event
pub fn new(
title: String,
description: String,
start_time: DateTime<Utc>,
end_time: DateTime<Utc>,
color: Option<String>,
all_day: bool,
user_id: Option<String>,
) -> Self {
Self {
id: Uuid::new_v4().to_string(),
title,
description,
start_time,
end_time,
color: color.unwrap_or_else(|| "#4285F4".to_string()), // Google Calendar blue
all_day,
user_id,
}
}
/// Converts the event to a JSON string
pub fn to_json(&self) -> Result<String, serde_json::Error> {
serde_json::to_string(self)
}
/// Creates an event from a JSON string
pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
serde_json::from_str(json)
}
}
/// Represents a view mode for the calendar /// Represents a view mode for the calendar
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]

View File

@ -85,6 +85,7 @@ pub struct ContractSigner {
pub comments: Option<String>, pub comments: Option<String>,
} }
#[allow(dead_code)]
impl ContractSigner { impl ContractSigner {
/// Creates a new contract signer /// Creates a new contract signer
pub fn new(name: String, email: String) -> Self { pub fn new(name: String, email: String) -> Self {
@ -123,6 +124,7 @@ pub struct ContractRevision {
pub comments: Option<String>, pub comments: Option<String>,
} }
#[allow(dead_code)]
impl ContractRevision { impl ContractRevision {
/// Creates a new contract revision /// Creates a new contract revision
pub fn new(version: u32, content: String, created_by: String, comments: Option<String>) -> Self { 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>>, pub toc: Option<Vec<TocItem>>,
} }
#[allow(dead_code)]
impl Contract { impl Contract {
/// Creates a new contract /// Creates a new contract
pub fn new(title: String, description: String, contract_type: ContractType, created_by: String, organization_id: Option<String>) -> Self { pub fn new(title: String, description: String, contract_type: ContractType, created_by: String, organization_id: Option<String>) -> Self {

View File

@ -14,6 +14,7 @@ pub enum DefiPositionStatus {
Cancelled Cancelled
} }
#[allow(dead_code)]
impl DefiPositionStatus { impl DefiPositionStatus {
pub fn as_str(&self) -> &str { pub fn as_str(&self) -> &str {
match self { match self {
@ -35,6 +36,7 @@ pub enum DefiPositionType {
Collateral, Collateral,
} }
#[allow(dead_code)]
impl DefiPositionType { impl DefiPositionType {
pub fn as_str(&self) -> &str { pub fn as_str(&self) -> &str {
match self { match self {
@ -95,6 +97,7 @@ pub struct DefiDatabase {
receiving_positions: HashMap<String, ReceivingPosition>, receiving_positions: HashMap<String, ReceivingPosition>,
} }
#[allow(dead_code)]
impl DefiDatabase { impl DefiDatabase {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {

View File

@ -110,6 +110,7 @@ pub struct FlowStep {
pub logs: Vec<FlowLog>, pub logs: Vec<FlowLog>,
} }
#[allow(dead_code)]
impl FlowStep { impl FlowStep {
/// Creates a new flow step /// Creates a new flow step
pub fn new(name: String, description: String, order: u32) -> Self { pub fn new(name: String, description: String, order: u32) -> Self {
@ -189,6 +190,7 @@ pub struct FlowLog {
pub timestamp: DateTime<Utc>, pub timestamp: DateTime<Utc>,
} }
#[allow(dead_code)]
impl FlowLog { impl FlowLog {
/// Creates a new flow log /// Creates a new flow log
pub fn new(message: String) -> Self { pub fn new(message: String) -> Self {
@ -231,6 +233,7 @@ pub struct Flow {
pub current_step: Option<FlowStep>, pub current_step: Option<FlowStep>,
} }
#[allow(dead_code)]
impl Flow { impl Flow {
/// Creates a new flow /// Creates a new flow
pub fn new(name: &str, description: &str, flow_type: FlowType, owner_id: &str, owner_name: &str) -> Self { pub fn new(name: &str, description: &str, flow_type: FlowType, owner_id: &str, owner_name: &str) -> Self {

View File

@ -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
}
}

View File

@ -1,7 +1,7 @@
use crate::models::asset::AssetType;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use uuid::Uuid; use uuid::Uuid;
use crate::models::asset::{Asset, AssetType};
/// Status of a marketplace listing /// Status of a marketplace listing
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
@ -12,6 +12,7 @@ pub enum ListingStatus {
Expired, Expired,
} }
#[allow(dead_code)]
impl ListingStatus { impl ListingStatus {
pub fn as_str(&self) -> &str { pub fn as_str(&self) -> &str {
match self { match self {
@ -63,6 +64,7 @@ pub enum BidStatus {
Cancelled, Cancelled,
} }
#[allow(dead_code)]
impl BidStatus { impl BidStatus {
pub fn as_str(&self) -> &str { pub fn as_str(&self) -> &str {
match self { match self {
@ -103,6 +105,7 @@ pub struct Listing {
pub image_url: Option<String>, pub image_url: Option<String>,
} }
#[allow(dead_code)]
impl Listing { impl Listing {
/// Creates a new listing /// Creates a new listing
pub fn new( pub fn new(
@ -150,7 +153,13 @@ impl Listing {
} }
/// Adds a bid to the 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 { if self.status != ListingStatus::Active {
return Err("Listing is not active".to_string()); return Err("Listing is not active".to_string());
} }
@ -160,7 +169,10 @@ impl Listing {
} }
if currency != self.currency { 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 // 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 /// Gets the highest bid on the listing
pub fn highest_bid(&self) -> Option<&Bid> { pub fn highest_bid(&self) -> Option<&Bid> {
self.bids.iter() self.bids
.iter()
.filter(|b| b.status == BidStatus::Active) .filter(|b| b.status == BidStatus::Active)
.max_by(|a, b| a.amount.partial_cmp(&b.amount).unwrap()) .max_by(|a, b| a.amount.partial_cmp(&b.amount).unwrap())
} }
/// Marks the listing as sold /// 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 { if self.status != ListingStatus::Active {
return Err("Listing is not active".to_string()); 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 listings_by_type = std::collections::HashMap::new();
let mut sales_by_asset_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) .filter(|l| l.status == ListingStatus::Active)
.count(); .count();
let sold_listings = listings.iter() let sold_listings = listings
.iter()
.filter(|l| l.status == ListingStatus::Sold) .filter(|l| l.status == ListingStatus::Sold)
.count(); .count();

View File

@ -1,17 +1,16 @@
// Export models // 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 asset;
pub mod marketplace; pub mod calendar;
pub mod contract;
pub mod defi; pub mod defi;
pub mod flow;
pub mod marketplace;
pub mod ticket;
pub mod user;
// Re-export models for easier imports // 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 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};

View File

@ -76,6 +76,7 @@ pub struct Ticket {
pub assigned_to: Option<i32>, pub assigned_to: Option<i32>,
} }
#[allow(dead_code)]
impl Ticket { impl Ticket {
/// Creates a new ticket /// Creates a new ticket
pub fn new(user_id: i32, title: String, description: String, priority: TicketPriority) -> Self { pub fn new(user_id: i32, title: String, description: String, priority: TicketPriority) -> Self {

View File

@ -4,6 +4,7 @@ use bcrypt::{hash, verify, DEFAULT_COST};
/// Represents a user in the system /// Represents a user in the system
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
#[allow(dead_code)]
pub struct User { pub struct User {
/// Unique identifier for the user /// Unique identifier for the user
pub id: Option<i32>, pub id: Option<i32>,
@ -31,6 +32,7 @@ pub enum UserRole {
Admin, Admin,
} }
#[allow(dead_code)]
impl User { impl User {
/// Creates a new user with default values /// Creates a new user with default values
pub fn new(name: String, email: String) -> Self { pub fn new(name: String, email: String) -> Self {
@ -125,6 +127,7 @@ impl User {
/// Represents user login credentials /// Represents user login credentials
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub struct LoginCredentials { pub struct LoginCredentials {
pub email: String, pub email: String,
pub password: String, pub password: String,
@ -132,6 +135,7 @@ pub struct LoginCredentials {
/// Represents user registration data /// Represents user registration data
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub struct RegistrationData { pub struct RegistrationData {
pub name: String, pub name: String,
pub email: String, pub email: String,

View File

@ -1,26 +1,24 @@
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::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 /// Configures all application routes
pub fn configure_routes(cfg: &mut web::ServiceConfig) { pub fn configure_routes(cfg: &mut web::ServiceConfig) {
// Configure session middleware with the consistent key // Configure session middleware with the consistent key
let session_middleware = SessionMiddleware::builder( let session_middleware =
CookieSessionStore::default(), SessionMiddleware::builder(CookieSessionStore::default(), SESSION_KEY.clone())
SESSION_KEY.clone()
)
.cookie_secure(false) // Set to true in production with HTTPS .cookie_secure(false) // Set to true in production with HTTPS
.build(); .build();
@ -33,56 +31,98 @@ pub fn configure_routes(cfg: &mut web::ServiceConfig) {
.route("/about", web::get().to(HomeController::about)) .route("/about", web::get().to(HomeController::about))
.route("/contact", web::get().to(HomeController::contact)) .route("/contact", web::get().to(HomeController::contact))
.route("/contact", web::post().to(HomeController::submit_contact)) .route("/contact", web::post().to(HomeController::submit_contact))
// Auth routes // Auth routes
.route("/login", web::get().to(AuthController::login_page)) .route("/login", web::get().to(AuthController::login_page))
.route("/login", web::post().to(AuthController::login)) .route("/login", web::post().to(AuthController::login))
.route("/register", web::get().to(AuthController::register_page)) .route("/register", web::get().to(AuthController::register_page))
.route("/register", web::post().to(AuthController::register)) .route("/register", web::post().to(AuthController::register))
.route("/logout", web::get().to(AuthController::logout)) .route("/logout", web::get().to(AuthController::logout))
// Protected routes that require authentication // Protected routes that require authentication
// These routes will be protected by the JwtAuth middleware in the main.rs file // These routes will be protected by the JwtAuth middleware in the main.rs file
.route("/editor", web::get().to(HomeController::editor)) .route("/editor", web::get().to(HomeController::editor))
// Ticket routes // Ticket routes
.route("/tickets", web::get().to(TicketController::list_tickets)) .route("/tickets", web::get().to(TicketController::list_tickets))
.route("/tickets/new", web::get().to(TicketController::new_ticket)) .route("/tickets/new", web::get().to(TicketController::new_ticket))
.route("/tickets", web::post().to(TicketController::create_ticket)) .route("/tickets", web::post().to(TicketController::create_ticket))
.route("/tickets/{id}", web::get().to(TicketController::show_ticket)) .route(
.route("/tickets/{id}/comment", web::post().to(TicketController::add_comment)) "/tickets/{id}",
.route("/tickets/{id}/status/{status}", web::post().to(TicketController::update_status)) 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)) .route("/my-tickets", web::get().to(TicketController::my_tickets))
// Calendar routes // Calendar routes
.route("/calendar", web::get().to(CalendarController::calendar)) .route("/calendar", web::get().to(CalendarController::calendar))
.route("/calendar/events/new", web::get().to(CalendarController::new_event)) .route(
.route("/calendar/events", web::post().to(CalendarController::create_event)) "/calendar/events/new",
.route("/calendar/events/{id}/delete", web::post().to(CalendarController::delete_event)) 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 // Governance routes
.route("/governance", web::get().to(GovernanceController::index)) .route("/governance", web::get().to(GovernanceController::index))
.route("/governance/proposals", web::get().to(GovernanceController::proposals)) .route(
.route("/governance/proposals/{id}", web::get().to(GovernanceController::proposal_detail)) "/governance/proposals",
.route("/governance/proposals/{id}/vote", web::post().to(GovernanceController::submit_vote)) web::get().to(GovernanceController::proposals),
.route("/governance/create", web::get().to(GovernanceController::create_proposal_form)) )
.route("/governance/create", web::post().to(GovernanceController::submit_proposal)) .route(
.route("/governance/my-votes", web::get().to(GovernanceController::my_votes)) "/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 // Flow routes
.service( .service(
web::scope("/flows") web::scope("/flows")
.route("", web::get().to(FlowController::index)) .route("", web::get().to(FlowController::index))
.route("/list", web::get().to(FlowController::list_flows)) .route("/list", web::get().to(FlowController::list_flows))
.route("/{id}", web::get().to(FlowController::flow_detail)) .route("/{id}", web::get().to(FlowController::flow_detail))
.route("/{id}/advance", web::post().to(FlowController::advance_flow_step)) .route(
.route("/{id}/stuck", web::post().to(FlowController::mark_flow_step_stuck)) "/{id}/advance",
.route("/{id}/step/{step_id}/log", web::post().to(FlowController::add_log_to_flow_step)) 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::get().to(FlowController::create_flow_form))
.route("/create", web::post().to(FlowController::create_flow)) .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 // Contract routes
.service( .service(
web::scope("/contracts") web::scope("/contracts")
@ -91,9 +131,8 @@ pub fn configure_routes(cfg: &mut web::ServiceConfig) {
.route("/my", web::get().to(ContractController::my_contracts)) .route("/my", web::get().to(ContractController::my_contracts))
.route("/{id}", web::get().to(ContractController::detail)) .route("/{id}", web::get().to(ContractController::detail))
.route("/create", web::get().to(ContractController::create_form)) .route("/create", web::get().to(ContractController::create_form))
.route("/create", web::post().to(ContractController::create)) .route("/create", web::post().to(ContractController::create)),
) )
// Asset routes // Asset routes
.service( .service(
web::scope("/assets") web::scope("/assets")
@ -104,35 +143,72 @@ pub fn configure_routes(cfg: &mut web::ServiceConfig) {
.route("/create", web::post().to(AssetController::create)) .route("/create", web::post().to(AssetController::create))
.route("/test", web::get().to(AssetController::test)) .route("/test", web::get().to(AssetController::test))
.route("/{id}", web::get().to(AssetController::detail)) .route("/{id}", web::get().to(AssetController::detail))
.route("/{id}/valuation", web::post().to(AssetController::add_valuation)) .route(
.route("/{id}/transaction", web::post().to(AssetController::add_transaction)) "/{id}/valuation",
.route("/{id}/status/{status}", web::post().to(AssetController::update_status)) 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 // Marketplace routes
.service( .service(
web::scope("/marketplace") web::scope("/marketplace")
.route("", web::get().to(MarketplaceController::index)) .route("", web::get().to(MarketplaceController::index))
.route("/listings", web::get().to(MarketplaceController::list_listings)) .route(
.route("/my", web::get().to(MarketplaceController::my_listings)) "/listings",
.route("/create", web::get().to(MarketplaceController::create_listing_form)) web::get().to(MarketplaceController::list_listings),
.route("/create", web::post().to(MarketplaceController::create_listing)) )
.route("/{id}", web::get().to(MarketplaceController::listing_detail)) .route("/my", web::get().to(MarketplaceController::my_listings))
.route("/{id}/bid", web::post().to(MarketplaceController::submit_bid)) .route(
.route("/{id}/purchase", web::post().to(MarketplaceController::purchase_listing)) "/create",
.route("/{id}/cancel", web::post().to(MarketplaceController::cancel_listing)) 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 // DeFi routes
.service( .service(
web::scope("/defi") web::scope("/defi")
.route("", web::get().to(DefiController::index)) .route("", web::get().to(DefiController::index))
.route("/providing", web::post().to(DefiController::create_providing)) .route(
.route("/receiving", web::post().to(DefiController::create_receiving)) "/providing",
web::post().to(DefiController::create_providing),
)
.route(
"/receiving",
web::post().to(DefiController::create_receiving),
)
.route("/liquidity", web::post().to(DefiController::add_liquidity)) .route("/liquidity", web::post().to(DefiController::add_liquidity))
.route("/staking", web::post().to(DefiController::create_staking)) .route("/staking", web::post().to(DefiController::create_staking))
.route("/swap", web::post().to(DefiController::swap_tokens)) .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 // Company routes
.service( .service(
@ -140,13 +216,15 @@ pub fn configure_routes(cfg: &mut web::ServiceConfig) {
.route("", web::get().to(CompanyController::index)) .route("", web::get().to(CompanyController::index))
.route("/register", web::post().to(CompanyController::register)) .route("/register", web::post().to(CompanyController::register))
.route("/view/{id}", web::get().to(CompanyController::view_company)) .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 // Keep the /protected scope for any future routes that should be under that path
cfg.service( cfg.service(
web::scope("/protected") web::scope("/protected").wrap(JwtAuth), // Apply JWT authentication middleware
.wrap(JwtAuth) // Apply JWT authentication middleware
); );
} }

View File

@ -1,16 +1,17 @@
use actix_web::{error, Error, HttpResponse}; use actix_web::{Error, HttpResponse};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use tera::{self, Context, Function, Tera, Value};
use std::error::Error as StdError; use std::error::Error as StdError;
use tera::{self, Context, Function, Tera, Value};
// Export modules // Export modules
pub mod redis_service; pub mod redis_service;
// Re-export for easier imports // Re-export for easier imports
pub use redis_service::RedisCalendarService; // pub use redis_service::RedisCalendarService; // Currently unused
/// Error type for template rendering /// Error type for template rendering
#[derive(Debug)] #[derive(Debug)]
#[allow(dead_code)]
pub struct TemplateError { pub struct TemplateError {
pub message: String, pub message: String,
pub details: String, pub details: String,
@ -25,10 +26,16 @@ impl std::fmt::Display for TemplateError {
impl std::error::Error 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) { pub fn register_tera_functions(tera: &mut tera::Tera) {
tera.register_function("now", NowFunction); tera.register_function("now", NowFunction);
tera.register_function("format_date", FormatDateFunction); 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 /// Tera function to get the current date/time
@ -68,14 +75,10 @@ impl Function for FormatDateFunction {
None => { None => {
return Err(tera::Error::msg( return Err(tera::Error::msg(
"The 'timestamp' argument must be a valid timestamp", "The 'timestamp' argument must be a valid timestamp",
)) ));
} }
}, },
None => { None => return Err(tera::Error::msg("The 'timestamp' argument is required")),
return Err(tera::Error::msg(
"The 'timestamp' argument is required",
))
}
}; };
let format = match args.get("format") { let format = match args.get("format") {
@ -89,23 +92,130 @@ impl Function for FormatDateFunction {
// Convert timestamp to DateTime using the non-deprecated method // Convert timestamp to DateTime using the non-deprecated method
let datetime = match DateTime::from_timestamp(timestamp, 0) { let datetime = match DateTime::from_timestamp(timestamp, 0) {
Some(dt) => dt, Some(dt) => dt,
None => { None => return Err(tera::Error::msg("Failed to convert timestamp to datetime")),
return Err(tera::Error::msg(
"Failed to convert timestamp to datetime",
))
}
}; };
Ok(Value::String(datetime.format(format).to_string())) 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 /// Formats a date for display
#[allow(dead_code)]
pub fn format_date(date: &DateTime<Utc>, format: &str) -> String { pub fn format_date(date: &DateTime<Utc>, format: &str) -> String {
date.format(format).to_string() date.format(format).to_string()
} }
/// Truncates a string to a maximum length and adds an ellipsis if truncated /// 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 { pub fn truncate_string(s: &str, max_length: usize) -> String {
if s.len() <= max_length { if s.len() <= max_length {
s.to_string() s.to_string()
@ -136,10 +246,13 @@ pub fn render_template(
Ok(content) => { Ok(content) => {
println!("DEBUG: Successfully rendered template: {}", template_name); println!("DEBUG: Successfully rendered template: {}", template_name);
Ok(HttpResponse::Ok().content_type("text/html").body(content)) Ok(HttpResponse::Ok().content_type("text/html").body(content))
}, }
Err(e) => { Err(e) => {
// Log the error with more details // 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); println!("DEBUG: Error details: {:?}", e);
// Print the error cause chain for better debugging // Print the error cause chain for better debugging

View File

@ -1,7 +1,7 @@
use heromodels::models::Event as CalendarEvent;
use lazy_static::lazy_static;
use redis::{Client, Commands, Connection, RedisError}; use redis::{Client, Commands, Connection, RedisError};
use std::sync::{Arc, Mutex}; 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 // Create a lazy static Redis client that can be used throughout the application
lazy_static! { lazy_static! {
@ -59,11 +59,11 @@ impl RedisCalendarService {
})?; })?;
// Save the event // 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)?; let _: () = conn.set(event_key, json)?;
// Add the event ID to the set of all events // 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(()) Ok(())
} }

File diff suppressed because it is too large Load Diff

View File

@ -12,7 +12,7 @@
</div> </div>
{% endif %} {% endif %}
<form action="/calendar/new" method="post"> <form action="/calendar/events" method="post">
<div class="mb-3"> <div class="mb-3">
<label for="title" class="form-label">Event Title</label> <label for="title" class="form-label">Event Title</label>
<input type="text" class="form-control" id="title" name="title" required> <input type="text" class="form-control" id="title" name="title" required>
@ -39,6 +39,13 @@
</div> </div>
</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"> <div class="mb-3">
<label for="color" class="form-label">Event Color</label> <label for="color" class="form-label">Event Color</label>
<select class="form-control" id="color" name="color"> <select class="form-control" id="color" name="color">
@ -60,6 +67,69 @@
<script> <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 // 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(); e.preventDefault();
@ -67,6 +137,12 @@
const startTime = document.getElementById('start_time').value; const startTime = document.getElementById('start_time').value;
const endTime = document.getElementById('end_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 // Convert to RFC3339 format
const startRFC = new Date(startTime).toISOString(); const startRFC = new Date(startTime).toISOString();
const endRFC = new Date(endTime).toISOString(); const endRFC = new Date(endTime).toISOString();

View 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>

View 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>

View 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 %}

View File

@ -4,53 +4,54 @@
{% block content %} {% block content %}
<div class="container-fluid"> <div class="container-fluid">
<div class="row mb-4"> <!-- Header -->
<div class="col-12"> {% include "governance/_header.html" %}
<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>
<!-- Navigation Tabs --> <!-- Navigation Tabs -->
<div class="row mb-4"> {% include "governance/_tabs.html" %}
<!-- Info Alert -->
<div class="row">
<div class="col-12"> <div class="col-12">
<ul class="nav nav-tabs"> <div class="alert alert-info alert-dismissible fade show">
<li class="nav-item"> <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
<a class="nav-link" href="/governance">Dashboard</a> <h5><i class="bi bi-info-circle"></i> About Creating Proposals</h5>
</li> <p>Creating a proposal is an important step in our community governance process. Well-crafted proposals
<li class="nav-item"> clearly state the problem, solution, and implementation details. The community will review and vote
<a class="nav-link" href="/governance/proposals">All Proposals</a> on your proposal, so be thorough and thoughtful in your submission.</p>
</li> <div class="mt-2">
<li class="nav-item"> <a href="/governance/proposal-templates" class="btn btn-sm btn-outline-primary"><i
<a class="nav-link" href="/governance/my-votes">My Votes</a> class="bi bi-file-earmark-text"></i> Proposal Templates</a>
</li> </div>
<li class="nav-item"> </div>
<a class="nav-link active" href="/governance/create">Create Proposal</a>
</li>
</ul>
</div> </div>
</div> </div>
<!-- Proposal Form --> <!-- Proposal Form and Guidelines in Flex Layout -->
<div class="row mb-4"> <div class="row mb-4">
<div class="col-md-8 mx-auto"> <!-- Proposal Form Column -->
<div class="card"> <div class="col-lg-8">
<div class="card h-100">
<div class="card-header"> <div class="card-header">
<h5 class="mb-0">New Proposal</h5> <h5 class="mb-0">New Proposal</h5>
</div> </div>
<div class="card-body"> <div class="card-body">
<form action="/governance/create" method="post"> <form action="/governance/create" method="post" id="proposalForm" novalidate>
<div class="mb-3"> <div class="mb-3">
<label for="title" class="form-label">Title</label> <label for="title" class="form-label">Title</label>
<input type="text" class="form-control" id="title" name="title" required <input type="text" class="form-control" id="title" name="title" required minlength="5"
placeholder="Enter a clear, concise title for your proposal"> 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 class="form-text">Make it descriptive and specific</div>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="description" class="form-label">Description</label> <label for="description" class="form-label">Description</label>
<textarea class="form-control" id="description" name="description" rows="6" required <textarea class="form-control" id="description" name="description" rows="8" required
minlength="50" maxlength="5000"
placeholder="Provide a detailed description of your proposal..."></textarea> 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 class="form-text">Explain the purpose, benefits, and implementation details</div>
</div> </div>
@ -58,11 +59,15 @@
<div class="col-md-6"> <div class="col-md-6">
<label for="voting_start_date" class="form-label">Voting Start Date</label> <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"> <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 class="form-text">When should voting begin?</div>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label for="voting_end_date" class="form-label">Voting End Date</label> <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"> <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 class="form-text">When should voting end?</div>
</div> </div>
</div> </div>
@ -84,12 +89,10 @@
</div> </div>
</div> </div>
</div> </div>
</div>
<!-- Guidelines Card --> <!-- Guidelines Column -->
<div class="row mb-4"> <div class="col-lg-4">
<div class="col-md-8 mx-auto"> <div class="card bg-light h-100">
<div class="card bg-light">
<div class="card-header"> <div class="card-header">
<h5 class="mb-0">Proposal Guidelines</h5> <h5 class="mb-0">Proposal Guidelines</h5>
</div> </div>
@ -116,4 +119,111 @@
</div> </div>
</div> </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 %} {% endblock %}

View File

@ -3,25 +3,11 @@
{% block title %}Governance Dashboard{% endblock %} {% block title %}Governance Dashboard{% endblock %}
{% block content %} {% block content %}
<!-- Header -->
{% include "governance/_header.html" %}
<!-- Navigation Tabs --> <!-- Navigation Tabs -->
<div class="row mb-3"> {% include "governance/_tabs.html" %}
<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>
</div>
</div>
<!-- Info Alert --> <!-- Info Alert -->
<div class="row mb-2"> <div class="row mb-2">
@ -29,9 +15,12 @@
<div class="alert alert-info alert-dismissible fade show"> <div class="alert alert-info alert-dismissible fade show">
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> <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> <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> <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"> <div class="mt-2">
<a href="/governance/documentation" class="btn btn-sm btn-outline-primary"><i class="bi bi-book"></i> Read Documentation</a> <a href="/governance/documentation" class="btn btn-sm btn-outline-primary"><i class="bi bi-book"></i>
Read Documentation</a>
</div> </div>
</div> </div>
</div> </div>
@ -46,8 +35,10 @@
<div class="card-header d-flex justify-content-between align-items-center"> <div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">Urgent: Voting Closes Soon</h5> <h5 class="mb-0">Urgent: Voting Closes Soon</h5>
<div> <div>
<span class="badge bg-warning text-dark me-2">Ends: {{ nearest_proposal.voting_ends_at | date(format="%Y-%m-%d") }}</span> <span class="badge bg-warning text-dark me-2">Ends: {{ nearest_proposal.vote_end_date |
<a href="/governance/proposals/{{ nearest_proposal.id }}" class="btn btn-sm btn-outline-primary">View Full Proposal</a> 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> </div>
<div class="card-body"> <div class="card-body">
@ -58,26 +49,54 @@
<p>{{ nearest_proposal.description }}</p> <p>{{ nearest_proposal.description }}</p>
</div> </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 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-success" role="progressbar" style="width: {{ yes_percent }}%"
<div class="progress-bar bg-danger" role="progressbar" style="width: 35%" aria-valuenow="35" aria-valuemin="0" aria-valuemax="100">35% No</div> 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>
<div class="d-flex justify-content-between text-muted small mb-4"> <div class="d-flex justify-content-between text-muted small mb-4">
<span>26 votes cast</span> <span>{{ total_votes }} votes cast</span>
<span>Quorum: 75% reached</span> <span>Quorum: {% if total_votes >= 20 %}75% reached{% else %}Not reached{% endif %}</span>
</div> </div>
<div class="mb-4"> <div class="mb-4">
<h5 class="mb-3">Cast Your Vote</h5> <h5 class="mb-3">Cast Your Vote</h5>
<form> <form action="/governance/proposals/{{ nearest_proposal.base_data.id }}/vote" method="post">
<div class="mb-3"> <div class="mb-3">
<input type="text" class="form-control" placeholder="Optional comment on your vote" aria-label="Vote comment"> <input type="text" class="form-control" name="comment"
placeholder="Optional comment on your vote" aria-label="Vote comment">
</div> </div>
<div class="d-flex justify-content-between"> <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_type" 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_type" value="No" class="btn btn-danger">Vote No</button>
<button type="submit" name="vote" value="abstain" class="btn btn-secondary">Abstain</button> <button type="submit" name="vote_type" value="Abstain"
class="btn btn-secondary">Abstain</button>
</div> </div>
</form> </form>
</div> </div>
@ -112,9 +131,11 @@
<div> <div>
<div class="d-flex justify-content-between align-items-center"> <div class="d-flex justify-content-between align-items-center">
<strong>{{ activity.user }}</strong> <strong>{{ activity.user }}</strong>
<small class="text-muted">{{ activity.timestamp | date(format="%H:%M") }}</small> <small class="text-muted">{{ activity.created_at | date(format="%H:%M") }}</small>
</div> </div>
<p class="mb-1">{{ activity.action }} on <a href="/governance/proposals/{{ activity.proposal_id }}">{{ activity.proposal_title }}</a></p> <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 %} {% if activity.type == "comment" and activity.comment is defined %}
<p class="mb-0 small text-muted">"{{ activity.comment }}"</p> <p class="mb-0 small text-muted">"{{ activity.comment }}"</p>
{% endif %} {% endif %}
@ -125,7 +146,7 @@
</div> </div>
</div> </div>
<div class="card-footer text-center"> <div class="card-footer text-center">
<a href="/governance/proposals" class="btn btn-sm btn-outline-info">View All Activity</a> <a href="/governance/activities" class="btn btn-sm btn-outline-info">View All Activities</a>
</div> </div>
</div> </div>
</div> </div>
@ -142,22 +163,23 @@
<div class="row"> <div class="row">
{% set count = 0 %} {% set count = 0 %}
{% for proposal in proposals %} {% for proposal in proposals %}
{% if count < 3 %} {% if count < 3 %} <div class="col-md-4 mb-3">
<div class="col-md-4 mb-3">
<div class="card h-100"> <div class="card h-100">
<div class="card-body"> <div class="card-body">
<h5 class="card-title">{{ proposal.title }}</h5> <h5 class="card-title">{{ proposal.title }}</h5>
<h6 class="card-subtitle mb-2 text-muted">By {{ proposal.creator_name }}</h6> <h6 class="card-subtitle mb-2 text-muted">By {{ proposal.creator_name }}</h6>
<p class="card-text">{{ proposal.description | truncate(length=100) }}</p> <p class="card-text">{{ proposal.description | truncate(length=100) }}</p>
<div class="d-flex justify-content-between align-items-center"> <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 %}"> <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 }} {{ proposal.status }}
</span> </span>
<a href="/governance/proposals/{{ proposal.id }}" class="btn btn-sm btn-outline-primary">View Details</a> <a href="/governance/proposals/{{ proposal.base_data.id }}"
class="btn btn-sm btn-outline-primary">View Details</a>
</div> </div>
</div> </div>
<div class="card-footer text-muted text-center"> <div class="card-footer text-muted text-center">
<span>Voting ends: {{ proposal.voting_ends_at | date(format="%Y-%m-%d") }}</span> <span>Voting ends: {{ proposal.vote_end_date | date(format="%Y-%m-%d") }}</span>
</div> </div>
</div> </div>
</div> </div>

View File

@ -3,23 +3,60 @@
{% block title %}My Votes - Governance Dashboard{% endblock %} {% block title %}My Votes - Governance Dashboard{% endblock %}
{% block content %} {% block content %}
<!-- Header -->
{% include "governance/_header.html" %}
<!-- Navigation Tabs --> <!-- Navigation Tabs -->
<div class="row mb-4"> {% include "governance/_tabs.html" %}
<!-- Info Alert -->
<div class="row">
<div class="col-12"> <div class="col-12">
<ul class="nav nav-tabs"> <div class="alert alert-info alert-dismissible fade show">
<li class="nav-item"> <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
<a class="nav-link" href="/governance">Dashboard</a> <h5><i class="bi bi-info-circle"></i> About Votes</h5>
</li> <p>Voting is a fundamental right of all token holders in our governance system. Each vote carries weight
<li class="nav-item"> proportional to your token holdings, ensuring fair representation. The voting statistics below show the
<a class="nav-link" href="/governance/proposals">All Proposals</a> community's collective decision-making across all proposals.</p>
</li> <div class="mt-2">
<li class="nav-item"> <a href="/governance/voting-guide" class="btn btn-sm btn-outline-primary"><i
<a class="nav-link active" href="/governance/my-votes">My Votes</a> class="bi bi-check2-square"></i> Voting Guide</a>
</li> </div>
<li class="nav-item"> </div>
<a class="nav-link" href="/governance/create">Create Proposal</a> </div>
</li> </div>
</ul>
<!-- 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>
</div> </div>
@ -48,18 +85,21 @@
<tr> <tr>
<td>{{ proposal.title }}</td> <td>{{ proposal.title }}</td>
<td> <td>
<span class="badge {% if vote.vote_type == 'Yes' %}bg-success{% elif vote.vote_type == 'No' %}bg-danger{% else %}bg-secondary{% endif %}"> <span
class="badge {% if vote.vote_type == 'Yes' %}bg-success{% elif vote.vote_type == 'No' %}bg-danger{% else %}bg-secondary{% endif %}">
{{ vote.vote_type }} {{ vote.vote_type }}
</span> </span>
</td> </td>
<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 %}"> <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 }} {{ proposal.status }}
</span> </span>
</td> </td>
<td>{{ vote.created_at | date(format="%Y-%m-%d") }}</td> <td>{{ vote.created_at | date(format="%Y-%m-%d") }}</td>
<td> <td>
<a href="/governance/proposals/{{ proposal.id }}" class="btn btn-sm btn-primary">View Proposal</a> <a href="/governance/proposals/{{ proposal.base_data.id }}"
class="btn btn-sm btn-primary">View Proposal</a>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
@ -79,57 +119,5 @@
</div> </div>
</div> </div>
<!-- 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>
</div>
</div>
</div>
{% endif %}
{% endblock %} {% endblock %}

View File

@ -2,8 +2,45 @@
{% block title %}{{ proposal.title }} - Governance Proposal{% endblock %} {% 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 %} {% block content %}
<div class="container-fluid"> <div class="container-fluid">
<!-- Header -->
{% include "governance/_header.html" %}
<!-- Navigation Tabs -->
{% include "governance/_tabs.html" %}
<div class="row mb-4"> <div class="row mb-4">
<div class="col-12"> <div class="col-12">
<nav aria-label="breadcrumb"> <nav aria-label="breadcrumb">
@ -30,42 +67,62 @@
<!-- Proposal Details --> <!-- Proposal Details -->
<div class="row mb-4"> <div class="row mb-4">
<div class="col-md-8"> <div class="col-lg-8">
<div class="card"> <div class="card h-100 shadow-sm">
<div class="card-header"> <div class="card-header bg-light">
<h4 class="mb-0">{{ proposal.title }}</h4> <h4 class="mb-0">{{ proposal.title }}</h4>
</div> </div>
<div class="card-body"> <div class="card-body d-flex flex-column">
<div class="d-flex justify-content-between mb-3"> <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"> <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 }} {{ proposal.status }}
</span> </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>
<h5>Description</h5> <div class="flex-grow-1">
<p class="mb-4">{{ proposal.description }}</p> <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>
<h5>Voting Period</h5> <div class="mt-auto">
<p> <h5><i class="bi bi-calendar-event me-2"></i>Voting Period</h5>
{% if proposal.voting_starts_at and proposal.voting_ends_at %} <div class="d-flex justify-content-between align-items-center p-3 bg-light rounded">
<strong>Start:</strong> {{ proposal.voting_starts_at | date(format="%Y-%m-%d") }} <br> {% if proposal.vote_start_date and proposal.vote_end_date %}
<strong>End:</strong> {{ proposal.voting_ends_at | date(format="%Y-%m-%d") }} <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 %} {% else %}
Not set <div class="text-center w-100">Not set</div>
{% endif %} {% endif %}
</p> </div>
</div>
</div> </div>
</div> </div>
</div> </div>
<div class="col-md-4"> <div class="col-lg-4">
<div class="card mb-4"> <div class="card mb-4 shadow-sm h-100">
<div class="card-header"> <div class="card-header bg-primary text-white">
<h5 class="mb-0">Voting Results</h5> <h5 class="mb-0"><i class="bi bi-bar-chart-fill me-2"></i>Voting Dashboard</h5>
</div> </div>
<div class="card-body"> <div class="card-body d-flex flex-column">
<div class="mb-3"> <!-- Voting Results Section -->
<div class="mb-4">
<h6 class="border-bottom pb-2 mb-3">Results</h6>
{% set yes_percent = 0 %} {% set yes_percent = 0 %}
{% set no_percent = 0 %} {% set no_percent = 0 %}
{% set abstain_percent = 0 %} {% set abstain_percent = 0 %}
@ -76,114 +133,483 @@
{% set abstain_percent = (results.abstain_count * 100 / results.total_votes) | int %} {% set abstain_percent = (results.abstain_count * 100 / results.total_votes) | int %}
{% endif %} {% endif %}
<p class="mb-1">Yes: {{ results.yes_count }} ({{ yes_percent }}%)</p> <!-- Yes votes -->
<div class="progress mb-3"> <div class="d-flex justify-content-between align-items-center mb-1">
<div class="progress-bar bg-success" role="progressbar" style="width: {{ yes_percent }}%"></div> <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> </div>
<p class="mb-1">No: {{ results.no_count }} ({{ no_percent }}%)</p> <!-- No votes -->
<div class="progress mb-3"> <div class="d-flex justify-content-between align-items-center mb-1">
<div class="progress-bar bg-danger" role="progressbar" style="width: {{ no_percent }}%"></div> <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> </div>
<p class="mb-1">Abstain: {{ results.abstain_count }} ({{ abstain_percent }}%)</p> <!-- Abstain votes -->
<div class="progress mb-3"> <div class="d-flex justify-content-between align-items-center mb-1">
<div class="progress-bar bg-secondary" role="progressbar" style="width: {{ abstain_percent }}%"></div> <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>
</div> <div class="progress mb-3" style="height: 12px;">
<p class="text-center"><strong>Total Votes:</strong> {{ results.total_votes }}</p> <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> </div>
<!-- Vote Form --> <div class="mt-auto">
{% if proposal.status == "Active" and user and user.id %} <div class="d-flex justify-content-between align-items-center p-3 bg-light rounded">
<div class="card"> <div class="text-center">
<div class="card-header"> <h4 class="mb-0">{{ results.total_votes }}</h4>
<h5 class="mb-0">Cast Your Vote</h5> <small class="text-muted">Total Votes</small>
</div> </div>
<div class="card-body">
<form action="/governance/proposals/{{ proposal.id }}/vote" method="post"> {% if proposal.status == "Active" %}
<div class="mb-3"> <div class="text-center">
<label class="form-label">Vote Type</label> <div class="position-relative d-inline-block" style="width: 60px; height: 60px;">
<div class="form-check"> <svg width="60" height="60">
<input class="form-check-input" type="radio" name="vote_type" id="voteYes" value="Yes" checked> <circle cx="30" cy="30" r="25" fill="none" stroke="#e9ecef" stroke-width="5">
<label class="form-check-label" for="voteYes"> </circle>
Yes - I support this proposal <circle cx="30" cy="30" r="25" fill="none" stroke="#0d6efd" stroke-width="5"
</label> stroke-dasharray="157"
</div> stroke-dashoffset="{{ 157 - (157 * yes_percent / 100) }}"
<div class="form-check"> transform="rotate(-90 30 30)"></circle>
<input class="form-check-input" type="radio" name="vote_type" id="voteNo" value="No"> </svg>
<label class="form-check-label" for="voteNo"> <div
No - I oppose this proposal class="position-absolute top-50 start-50 translate-middle text-primary fw-bold">
</label> {{ yes_percent }}%</div>
</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>
<small class="text-muted">Approval Rate</small>
</div> </div>
{% endif %} {% endif %}
</div> </div>
</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>
{% endif %}
</div>
</div>
</div>
<!-- Votes List --> <!-- Votes List -->
<div class="row mb-4"> <div class="row mt-4">
<div class="col-12"> <div class="col-12">
<div class="card"> <div class="card shadow-sm">
<div class="card-header"> <div class="card-header bg-light">
<h5 class="mb-0">Votes ({{ votes | length }})</h5> <h5 class="mb-0"><i class="bi bi-list-check me-2"></i>Votes</h5>
</div> </div>
<div class="card-body"> <div class="card-body p-0">
{% if votes | length > 0 %}
<div class="table-responsive"> <div class="table-responsive">
<table class="table"> <table class="table table-hover align-middle mb-0">
<thead> <thead class="table-light">
<tr> <tr>
<th>Voter</th> <th class="ps-3">Voter</th>
<th>Vote</th> <th>Vote</th>
<th>Comment</th> <th>Comment</th>
<th>Date</th> <th class="text-end pe-3">Date</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody id="votesTableBody">
{% for vote in votes %} {% if votes | length == 0 %}
<tr> <tr>
<td>{{ vote.voter_name }}</td> <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> <td>
<span class="badge {% if vote.vote_type == 'Yes' %}bg-success{% elif vote.vote_type == 'No' %}bg-danger{% else %}bg-secondary{% endif %}"> <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 }} {{ vote.vote_type }}
</span> </span>
</td> </td>
<td>{% if vote.comment %}{{ vote.comment }}{% else %}No comment{% endif %}</td> <td>
<td>{{ vote.created_at | date(format="%Y-%m-%d %H:%M") }}</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> </tr>
{% endfor %} {% endfor %}
{% endif %}
</tbody> </tbody>
</table> </table>
</div> </div>
{% else %}
<p class="text-center">No votes have been cast yet.</p> <!-- 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">&laquo;</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">&raquo;</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 %} {% endif %}
</div> </div>
</div> </div>
</div> </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 %} {% endblock %}

View File

@ -3,6 +3,12 @@
{% block title %}Proposals - Governance Dashboard{% endblock %} {% block title %}Proposals - Governance Dashboard{% endblock %}
{% block content %} {% block content %}
<!-- Header -->
{% include "governance/_header.html" %}
<!-- Navigation Tabs -->
{% include "governance/_tabs.html" %}
<!-- Success message if present --> <!-- Success message if present -->
{% if success %} {% if success %}
<div class="row mb-4"> <div class="row mb-4">
@ -15,33 +21,16 @@
</div> </div>
{% endif %} {% endif %}
<!-- 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>
<div class="col-12"> <div class="col-12">
<div class="alert alert-info alert-dismissible fade show"> <div class="alert alert-info alert-dismissible fade show">
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> <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> <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> <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"> <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> <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>
</div> </div>
@ -55,17 +44,23 @@
<div class="col-md-4"> <div class="col-md-4">
<label for="status" class="form-label">Status</label> <label for="status" class="form-label">Status</label>
<select class="form-select" id="status" name="status"> <select class="form-select" id="status" name="status">
<option value="">All Statuses</option> <option value="" {% if not status_filter or status_filter=="" %}selected{% endif %}>All
<option value="Draft">Draft</option> Statuses</option>
<option value="Active">Active</option> <option value="Draft" {% if status_filter=="Draft" %}selected{% endif %}>Draft</option>
<option value="Approved">Approved</option> <option value="Active" {% if status_filter=="Active" %}selected{% endif %}>Active</option>
<option value="Rejected">Rejected</option> <option value="Approved" {% if status_filter=="Approved" %}selected{% endif %}>Approved
<option value="Cancelled">Cancelled</option> </option>
<option value="Rejected" {% if status_filter=="Rejected" %}selected{% endif %}>Rejected
</option>
<option value="Cancelled" {% if status_filter=="Cancelled" %}selected{% endif %}>Cancelled
</option>
</select> </select>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label for="search" class="form-label">Search</label> <label for="search" class="form-label">Search</label>
<input type="text" class="form-control" id="search" name="search" placeholder="Search by title or description"> <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>
<div class="col-md-2 d-flex align-items-end"> <div class="col-md-2 d-flex align-items-end">
<button type="submit" class="btn btn-primary w-100">Filter</button> <button type="submit" class="btn btn-primary w-100">Filter</button>
@ -85,6 +80,7 @@
<a href="/governance/create" class="btn btn-sm btn-primary">Create New Proposal</a> <a href="/governance/create" class="btn btn-sm btn-primary">Create New Proposal</a>
</div> </div>
<div class="card-body"> <div class="card-body">
{% if proposals and proposals|length > 0 %}
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-hover"> <table class="table table-hover">
<thead> <thead>
@ -103,25 +99,41 @@
<td>{{ proposal.title }}</td> <td>{{ proposal.title }}</td>
<td>{{ proposal.creator_name }}</td> <td>{{ proposal.creator_name }}</td>
<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 %}"> <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 }} {{ proposal.status }}
</span> </span>
</td> </td>
<td>{{ proposal.created_at | date(format="%Y-%m-%d") }}</td> <td>{{ proposal.created_at | date(format="%Y-%m-%d") }}</td>
<td> <td>
{% if proposal.voting_starts_at and proposal.voting_ends_at %} {% if proposal.vote_start_date and proposal.vote_end_date %}
{{ proposal.voting_starts_at | date(format="%Y-%m-%d") }} to {{ proposal.voting_ends_at | date(format="%Y-%m-%d") }} {{ proposal.vote_start_date | date(format="%Y-%m-%d") }} to {{
proposal.vote_end_date | date(format="%Y-%m-%d") }}
{% else %} {% else %}
Not set Not set
{% endif %} {% endif %}
</td> </td>
<td> <td>
<a href="/governance/proposals/{{ proposal.id }}" class="btn btn-sm btn-primary">View</a> <a href="/governance/proposals/{{ proposal.base_data.id }}"
class="btn btn-sm btn-primary">View</a>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </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>
</div> </div>