diff --git a/actix_mvc_app/.cargo/config.toml b/actix_mvc_app/.cargo/config.toml new file mode 100644 index 0000000..c91c3f3 --- /dev/null +++ b/actix_mvc_app/.cargo/config.toml @@ -0,0 +1,2 @@ +[net] +git-fetch-with-cli = true diff --git a/actix_mvc_app/Cargo.lock b/actix_mvc_app/Cargo.lock index 169ea39..ee0c02c 100644 --- a/actix_mvc_app/Cargo.lock +++ b/actix_mvc_app/Cargo.lock @@ -296,6 +296,8 @@ dependencies = [ "env_logger", "futures", "futures-util", + "heromodels", + "heromodels_core", "jsonwebtoken", "lazy_static", "log", @@ -309,6 +311,14 @@ dependencies = [ "uuid", ] +[[package]] +name = "adapter_macros" +version = "0.1.0" +dependencies = [ + "chrono", + "rhai", +] + [[package]] name = "addr2line" version = "0.24.2" @@ -366,6 +376,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", + "const-random", + "getrandom 0.2.15", "once_cell", "version_check", "zerocopy 0.7.35", @@ -478,6 +490,12 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + [[package]] name = "async-trait" version = "0.1.88" @@ -547,6 +565,26 @@ dependencies = [ "zeroize", ] +[[package]] +name = "bincode" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740" +dependencies = [ + "bincode_derive", + "serde", + "unty", +] + +[[package]] +name = "bincode_derive" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf95709a440f45e986983918d0e8a1f30a9b1df04918fc828670606804ac3c09" +dependencies = [ + "virtue", +] + [[package]] name = "bitflags" version = "2.9.0" @@ -1285,12 +1323,62 @@ dependencies = [ "hashbrown 0.14.5", ] +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +[[package]] +name = "heromodels" +version = "0.1.0" +dependencies = [ + "adapter_macros", + "bincode", + "chrono", + "heromodels-derive", + "heromodels_core", + "ourdb", + "rhai", + "rhai_autobind_macros", + "rhai_client_macros", + "rhai_wrapper", + "serde", + "serde_json", + "strum", + "strum_macros", + "tst", +] + +[[package]] +name = "heromodels-derive" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "heromodels_core" +version = "0.1.0" +dependencies = [ + "chrono", + "serde", +] + [[package]] name = "hkdf" version = "0.12.4" @@ -1557,6 +1645,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -1756,6 +1853,15 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "no-std-compat" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" +dependencies = [ + "spin", +] + [[package]] name = "nom" version = "7.1.3" @@ -1824,6 +1930,9 @@ name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +dependencies = [ + "portable-atomic", +] [[package]] name = "opaque-debug" @@ -1841,6 +1950,16 @@ dependencies = [ "hashbrown 0.14.5", ] +[[package]] +name = "ourdb" +version = "0.1.0" +dependencies = [ + "crc32fast", + "log", + "rand 0.8.5", + "thiserror 1.0.69", +] + [[package]] name = "parking_lot" version = "0.12.3" @@ -1907,7 +2026,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "198db74531d58c70a361c42201efde7e2591e976d518caf7662a47dc5720e7b6" dependencies = [ "memchr", - "thiserror", + "thiserror 2.0.12", "ucd-trie", ] @@ -2210,6 +2329,75 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "rhai" +version = "1.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce4d759a4729a655ddfdbb3ff6e77fb9eadd902dae12319455557796e435d2a6" +dependencies = [ + "ahash", + "bitflags", + "instant", + "no-std-compat", + "num-traits", + "once_cell", + "rhai_codegen", + "rust_decimal", + "smallvec", + "smartstring", + "thin-vec", +] + +[[package]] +name = "rhai_autobind_macros" +version = "0.1.0" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "rhai_client_macros" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "rhai", + "syn", +] + +[[package]] +name = "rhai_codegen" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5a11a05ee1ce44058fa3d5961d05194fdbe3ad6b40f904af764d81b86450e6b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "rhai_macros_derive" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "rhai_wrapper" +version = "0.1.0" +dependencies = [ + "chrono", + "rhai", + "rhai_macros_derive", + "serde", +] + [[package]] name = "ring" version = "0.16.20" @@ -2247,6 +2435,16 @@ dependencies = [ "ordered-multimap", ] +[[package]] +name = "rust_decimal" +version = "1.37.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faa7de2ba56ac291bd90c6b9bece784a52ae1411f9506544b3eae36dd2356d50" +dependencies = [ + "arrayvec", + "num-traits", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -2421,7 +2619,7 @@ checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" dependencies = [ "num-bigint", "num-traits", - "thiserror", + "thiserror 2.0.12", "time", ] @@ -2456,6 +2654,17 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" +[[package]] +name = "smartstring" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29" +dependencies = [ + "autocfg", + "static_assertions", + "version_check", +] + [[package]] name = "socket2" version = "0.4.10" @@ -2488,12 +2697,37 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + [[package]] name = "subtle" version = "2.6.1" @@ -2557,13 +2791,39 @@ dependencies = [ "unic-segment", ] +[[package]] +name = "thin-vec" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "144f754d318415ac792f9d69fc87abbbfc043ce2ef041c60f16ad828f638717d" + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.12", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -2723,6 +2983,14 @@ dependencies = [ "once_cell", ] +[[package]] +name = "tst" +version = "0.1.0" +dependencies = [ + "ourdb", + "thiserror 1.0.69", +] + [[package]] name = "typenum" version = "1.18.0" @@ -2831,6 +3099,12 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +[[package]] +name = "unty" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" + [[package]] name = "url" version = "2.5.4" @@ -2888,6 +3162,12 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "virtue" +version = "0.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" + [[package]] name = "walkdir" version = "2.5.0" diff --git a/actix_mvc_app/Cargo.toml b/actix_mvc_app/Cargo.toml index d39207f..499c686 100644 --- a/actix_mvc_app/Cargo.toml +++ b/actix_mvc_app/Cargo.toml @@ -15,6 +15,8 @@ env_logger = "0.11.2" log = "0.4.21" dotenv = "0.15.0" chrono = { version = "0.4.35", features = ["serde"] } +heromodels = { path = "../../db/heromodels" } +heromodels_core = { path = "../../db/heromodels_core" } config = "0.14.0" num_cpus = "1.16.0" futures = "0.3.30" @@ -27,3 +29,8 @@ redis = { version = "0.23.0", features = ["tokio-comp"] } jsonwebtoken = "8.3.0" pulldown-cmark = "0.13.0" urlencoding = "2.1.3" + +[patch."https://git.ourworld.tf/herocode/db.git"] +rhai_autobind_macros = { path = "../../rhaj/rhai_autobind_macros" } +rhai_wrapper = { path = "../../rhaj/rhai_wrapper" } + diff --git a/actix_mvc_app/src/config/mod.rs b/actix_mvc_app/src/config/mod.rs index 7c1cb06..87ab06d 100644 --- a/actix_mvc_app/src/config/mod.rs +++ b/actix_mvc_app/src/config/mod.rs @@ -1,6 +1,6 @@ -use std::env; use config::{Config, ConfigError, File}; use serde::Deserialize; +use std::env; /// Application configuration #[derive(Debug, Deserialize, Clone)] @@ -13,6 +13,7 @@ pub struct AppConfig { /// Server configuration #[derive(Debug, Deserialize, Clone)] +#[allow(dead_code)] pub struct ServerConfig { /// Host address to bind to pub host: String, @@ -50,7 +51,8 @@ impl AppConfig { } // Override with environment variables (e.g., SERVER__HOST, SERVER__PORT) - config_builder = config_builder.add_source(config::Environment::with_prefix("APP").separator("__")); + config_builder = + config_builder.add_source(config::Environment::with_prefix("APP").separator("__")); // Build and deserialize the config let config = config_builder.build()?; @@ -61,4 +63,4 @@ impl AppConfig { /// Returns the application configuration pub fn get_config() -> AppConfig { AppConfig::new().expect("Failed to load configuration") -} \ No newline at end of file +} diff --git a/actix_mvc_app/src/controllers/asset.rs b/actix_mvc_app/src/controllers/asset.rs index 6e937c6..741294f 100644 --- a/actix_mvc_app/src/controllers/asset.rs +++ b/actix_mvc_app/src/controllers/asset.rs @@ -1,12 +1,13 @@ -use actix_web::{web, HttpResponse, Result}; -use tera::{Context, Tera}; -use chrono::{Utc, Duration}; +use actix_web::{HttpResponse, Result, web}; +use chrono::{Duration, Utc}; use serde::Deserialize; +use tera::{Context, Tera}; -use crate::models::asset::{Asset, AssetType, AssetStatus, BlockchainInfo, ValuationPoint, AssetTransaction, AssetStatistics}; +use crate::models::asset::{Asset, AssetStatistics, AssetStatus, AssetType, BlockchainInfo}; use crate::utils::render_template; #[derive(Debug, Deserialize)] +#[allow(dead_code)] pub struct AssetForm { pub name: String, pub description: String, @@ -14,6 +15,7 @@ pub struct AssetForm { } #[derive(Debug, Deserialize)] +#[allow(dead_code)] pub struct ValuationForm { pub value: f64, pub currency: String, @@ -22,6 +24,7 @@ pub struct ValuationForm { } #[derive(Debug, Deserialize)] +#[allow(dead_code)] pub struct TransactionForm { pub transaction_type: String, pub from_address: Option, @@ -38,31 +41,31 @@ impl AssetController { // Display the assets dashboard pub async fn index(tmpl: web::Data) -> Result { let mut context = Context::new(); - + println!("DEBUG: Starting assets dashboard rendering"); - + let assets = Self::get_mock_assets(); println!("DEBUG: Generated {} mock assets", assets.len()); - + let stats = AssetStatistics::new(&assets); println!("DEBUG: Generated asset statistics: {:?}", stats); - + // Add active_page for navigation highlighting context.insert("active_page", &"assets"); - + // Add stats context.insert("stats", &serde_json::to_value(stats).unwrap()); println!("DEBUG: Added stats to context"); - + // Add recent assets let recent_assets: Vec> = assets .iter() .take(5) .map(|a| Self::asset_to_json(a)) .collect(); - + context.insert("recent_assets", &recent_assets); - + // Add assets by type let asset_types = vec![ AssetType::Artwork, @@ -74,139 +77,155 @@ impl AssetController { AssetType::IntellectualProperty, AssetType::Other, ]; - + let assets_by_type: Vec> = asset_types .iter() .map(|asset_type| { let mut map = serde_json::Map::new(); let type_str = asset_type.as_str(); - 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("count".to_string(), serde_json::Value::Number(serde_json::Number::from(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( + "count".to_string(), + serde_json::Value::Number(serde_json::Number::from(count)), + ); + map }) .collect(); - + context.insert("assets_by_type", &assets_by_type); - + println!("DEBUG: Rendering assets dashboard template"); let response = render_template(&tmpl, "assets/index.html", &context); println!("DEBUG: Finished rendering assets dashboard template"); response } - + // Display the list of all assets pub async fn list(tmpl: web::Data) -> Result { let mut context = Context::new(); - + println!("DEBUG: Starting assets list rendering"); - + let assets = Self::get_mock_assets(); println!("DEBUG: Generated {} mock assets", assets.len()); - - let assets_data: Vec> = assets - .iter() - .map(|a| Self::asset_to_json(a)) - .collect(); - + + let assets_data: Vec> = + assets.iter().map(|a| Self::asset_to_json(a)).collect(); + // Add active_page for navigation highlighting context.insert("active_page", &"assets"); - + context.insert("assets", &assets_data); context.insert("filter", &"all"); - + println!("DEBUG: Rendering assets list template"); let response = render_template(&tmpl, "assets/list.html", &context); println!("DEBUG: Finished rendering assets list template"); response } - + // Display the list of user's assets pub async fn my_assets(tmpl: web::Data) -> Result { let mut context = Context::new(); - + println!("DEBUG: Starting my assets rendering"); - + let assets = Self::get_mock_assets(); println!("DEBUG: Generated {} mock assets", assets.len()); - - let assets_data: Vec> = assets - .iter() - .map(|a| Self::asset_to_json(a)) - .collect(); - + + let assets_data: Vec> = + assets.iter().map(|a| Self::asset_to_json(a)).collect(); + // Add active_page for navigation highlighting context.insert("active_page", &"assets"); - + context.insert("assets", &assets_data); - + println!("DEBUG: Rendering my assets template"); let response = render_template(&tmpl, "assets/my_assets.html", &context); println!("DEBUG: Finished rendering my assets template"); response } - + // Display a specific asset pub async fn detail(tmpl: web::Data, path: web::Path) -> Result { let asset_id = path.into_inner(); let mut context = Context::new(); - + println!("DEBUG: Starting asset detail rendering"); - + // Add active_page for navigation highlighting context.insert("active_page", &"assets"); - + // Find the asset by ID let assets = Self::get_mock_assets(); let asset = assets.iter().find(|a| a.id == asset_id); - + match asset { Some(asset) => { println!("DEBUG: Found asset with ID {}", asset_id); - + // Convert asset to JSON let asset_json = Self::asset_to_json(asset); - + context.insert("asset", &asset_json); - + // Add valuation history for chart let valuation_history: Vec> = asset .sorted_valuation_history() .iter() .map(|v| { 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("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.insert( + "date".to_string(), + 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 }) .collect(); - + context.insert("valuation_history", &valuation_history); - + println!("DEBUG: Rendering asset detail template"); let response = render_template(&tmpl, "assets/detail.html", &context); println!("DEBUG: Finished rendering asset detail template"); response - }, + } None => { println!("DEBUG: Asset not found with ID {}", asset_id); Ok(HttpResponse::NotFound().finish()) } } } - + // Display the create asset form pub async fn create_form(tmpl: web::Data) -> Result { let mut context = Context::new(); - + println!("DEBUG: Starting create asset form rendering"); - + // Add active_page for navigation highlighting context.insert("active_page", &"assets"); - + // Add asset types for dropdown let asset_types = vec![ ("Artwork", "Artwork"), @@ -216,30 +235,32 @@ impl AssetController { ("Share", "Share"), ("Bond", "Bond"), ("IntellectualProperty", "Intellectual Property"), - ("Other", "Other") + ("Other", "Other"), ]; - + context.insert("asset_types", &asset_types); - + println!("DEBUG: Rendering create asset form template"); let response = render_template(&tmpl, "assets/create.html", &context); println!("DEBUG: Finished rendering create asset form template"); response } - + // Process the create asset form pub async fn create( _tmpl: web::Data, _form: web::Form, ) -> Result { println!("DEBUG: Processing create asset form"); - + // In a real application, we would save the asset to the database // 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 pub async fn add_valuation( _tmpl: web::Data, @@ -247,15 +268,17 @@ impl AssetController { _form: web::Form, ) -> Result { let asset_id = path.into_inner(); - + println!("DEBUG: Adding valuation to asset with ID {}", asset_id); - + // In a real application, we would update the asset in the database // 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 pub async fn add_transaction( _tmpl: web::Data, @@ -263,190 +286,309 @@ impl AssetController { _form: web::Form, ) -> Result { let asset_id = path.into_inner(); - + println!("DEBUG: Adding transaction to asset with ID {}", asset_id); - + // In a real application, we would update the asset in the database // 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 pub async fn update_status( _tmpl: web::Data, path: web::Path<(String, String)>, ) -> Result { let (asset_id, _status) = path.into_inner(); - + println!("DEBUG: Updating status of asset with ID {}", asset_id); - + // In a real application, we would update the asset in the database // 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 pub async fn test(tmpl: web::Data) -> Result { println!("DEBUG: Starting test page rendering"); - + let mut context = Context::new(); - + let assets = Self::get_mock_assets(); println!("DEBUG: Generated {} mock assets for test", assets.len()); - + let stats = AssetStatistics::new(&assets); println!("DEBUG: Generated asset statistics for test: {:?}", stats); - + // Add active_page for navigation highlighting context.insert("active_page", &"assets"); - + // Add stats context.insert("stats", &serde_json::to_value(stats).unwrap()); println!("DEBUG: Added stats to context for test"); - + // Add recent assets let recent_assets: Vec> = assets .iter() .take(5) .map(|a| Self::asset_to_json(a)) .collect(); - + context.insert("recent_assets", &recent_assets); - + println!("DEBUG: Rendering test_base.html with full context"); let response = render_template(&tmpl, "test_base.html", &context); println!("DEBUG: Finished rendering test_base.html"); response } - + // Helper method to convert Asset to a JSON object for templates fn asset_to_json(asset: &Asset) -> serde_json::Map { let mut map = serde_json::Map::new(); - - map.insert("id".to_string(), serde_json::Value::String(asset.id.clone())); - map.insert("name".to_string(), serde_json::Value::String(asset.name.clone())); - map.insert("description".to_string(), serde_json::Value::String(asset.description.clone())); - map.insert("asset_type".to_string(), serde_json::Value::String(asset.asset_type.as_str().to_string())); - map.insert("status".to_string(), serde_json::Value::String(asset.status.as_str().to_string())); - map.insert("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())); - + + map.insert( + "id".to_string(), + serde_json::Value::String(asset.id.clone()), + ); + map.insert( + "name".to_string(), + serde_json::Value::String(asset.name.clone()), + ); + map.insert( + "description".to_string(), + serde_json::Value::String(asset.description.clone()), + ); + map.insert( + "asset_type".to_string(), + serde_json::Value::String(asset.asset_type.as_str().to_string()), + ); + map.insert( + "status".to_string(), + serde_json::Value::String(asset.status.as_str().to_string()), + ); + map.insert( + "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 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 { - 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 { - 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 if let Some(blockchain_info) = &asset.blockchain_info { 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("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())); - + blockchain_map.insert( + "blockchain".to_string(), + serde_json::Value::String(blockchain_info.blockchain.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 { - 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 { - 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 { - 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 - let valuation_history: Vec = asset.valuation_history.iter() + let valuation_history: Vec = asset + .valuation_history + .iter() .map(|v| { let mut valuation_map = serde_json::Map::new(); 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("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())); - + valuation_map.insert( + "date".to_string(), + serde_json::Value::String(v.date.format("%Y-%m-%d").to_string()), + ); + 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 { - 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) }) .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 - let transaction_history: Vec = asset.transaction_history.iter() + let transaction_history: Vec = asset + .transaction_history + .iter() .map(|t| { let mut transaction_map = serde_json::Map::new(); 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("date".to_string(), serde_json::Value::String(t.date.format("%Y-%m-%d").to_string())); - + transaction_map.insert( + "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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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) }) .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 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 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 } - + // Generate mock assets for testing pub fn get_mock_assets() -> Vec { let now = Utc::now(); let mut assets = Vec::new(); - + // Create Tokenized Real Estate asset let mut zanzibar_resort = Asset { id: "asset-zanzibar-resort".to_string(), @@ -475,21 +617,38 @@ impl AssetController { image_url: Some("https://images.unsplash.com/photo-1506744038136-46273834b3fb?auto=format&fit=crop&w=600&q=80".to_string()), external_url: Some("https://oceanviewholdings.zdfz/resort".to_string()), }; - + zanzibar_resort.add_blockchain_info(BlockchainInfo { blockchain: "Ethereum".to_string(), token_id: "ZRESORT".to_string(), contract_address: "0x123456789abcdef123456789abcdef12345678ab".to_string(), owner_address: "0xc3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4".to_string(), - transaction_hash: Some("0xabcdef123456789abcdef123456789abcdef123456789abcdef123456789abcd".to_string()), + transaction_hash: Some( + "0xabcdef123456789abcdef123456789abcdef123456789abcdef123456789abcd".to_string(), + ), block_number: Some(9876543), 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(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_valuation( + 650000.0, + "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( "Tokenization", None, @@ -499,7 +658,7 @@ impl AssetController { Some("0xabcdef123456789abcdef123456789abcdef123456789abcdef123456789abcd".to_string()), Some("Initial property tokenization under ZDFZ Property Registry".to_string()), ); - + zanzibar_resort.add_transaction( "Token Sale", Some("0xc3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4".to_string()), @@ -509,9 +668,9 @@ impl AssetController { Some("0xdef123456789abcdef123456789abcdef123456789abcdef123456789abcdef".to_string()), Some("Sale of 10% ownership tokens to Zanzibar Investment Collective".to_string()), ); - + assets.push(zanzibar_resort); - + // Create ZDFZ Governance Token let mut zaz_token = Asset { id: "asset-zdfz-governance".to_string(), @@ -539,7 +698,7 @@ impl AssetController { image_url: Some("https://images.unsplash.com/photo-1431540015161-0bf868a2d407?q=80&w=3540&?auto=format&fit=crop&w=600&q=80".to_string()), external_url: Some("https://governance.zdfz/token".to_string()), }; - + zaz_token.add_blockchain_info(BlockchainInfo { blockchain: "ThreeFold".to_string(), token_id: "ZAZT".to_string(), @@ -549,11 +708,26 @@ impl AssetController { block_number: None, 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(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_valuation( + 300000.0, + "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( "Distribution", Some("0xe5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6".to_string()), @@ -563,7 +737,7 @@ impl AssetController { Some("0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234".to_string()), Some("Initial token distribution to founding members".to_string()), ); - + zaz_token.add_transaction( "Distribution", Some("0xe5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6".to_string()), @@ -573,9 +747,9 @@ impl AssetController { Some("0x234567890abcdef123456789abcdef123456789abcdef123456789abcdef1234".to_string()), Some("Public token sale for zone participants".to_string()), ); - + assets.push(zaz_token); - + // Create Spice Trade Venture Shares let mut spice_trade_shares = Asset { id: "asset-spice-trade-shares".to_string(), @@ -604,21 +778,38 @@ impl AssetController { image_url: Some("https://images.unsplash.com/photo-1464983953574-0892a716854b?auto=format&fit=crop&w=600&q=80".to_string()), external_url: Some("https://spicetrade.zdfz".to_string()), }; - + spice_trade_shares.add_blockchain_info(BlockchainInfo { blockchain: "Ethereum".to_string(), token_id: "SPICE".to_string(), contract_address: "0x3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4".to_string(), owner_address: "0x6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b".to_string(), - transaction_hash: Some("0x789abcdef123456789abcdef123456789abcdef123456789abcdef123456789a".to_string()), + transaction_hash: Some( + "0x789abcdef123456789abcdef123456789abcdef123456789abcdef123456789a".to_string(), + ), block_number: Some(7654321), 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(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_valuation( + 150000.0, + "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( "Share Issuance", None, @@ -628,7 +819,7 @@ impl AssetController { Some("0x789abcdef123456789abcdef123456789abcdef123456789abcdef123456789a".to_string()), Some("Initial share issuance at company formation".to_string()), ); - + spice_trade_shares.add_transaction( "Share Transfer", Some("0x6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b".to_string()), @@ -638,9 +829,9 @@ impl AssetController { Some("0x89abcdef123456789abcdef123456789abcdef123456789abcdef123456789ab".to_string()), Some("Sale of 25% equity to East African Growth Partners".to_string()), ); - + assets.push(spice_trade_shares); - + // Create Sustainable Energy Patent let mut tidal_energy_patent = Asset { id: "asset-tidal-energy-patent".to_string(), @@ -669,21 +860,38 @@ impl AssetController { image_url: Some("https://images.unsplash.com/photo-1708851148146-783a5b7da55d?q=80&w=3474&?auto=format&fit=crop&w=600&q=80".to_string()), external_url: Some("https://patents.zdfz/ZDFZ-PAT-2024-0142".to_string()), }; - + tidal_energy_patent.add_blockchain_info(BlockchainInfo { blockchain: "Polygon".to_string(), token_id: "TIDALIP".to_string(), contract_address: "0x2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3".to_string(), owner_address: "0x4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f".to_string(), - transaction_hash: Some("0x56789abcdef123456789abcdef123456789abcdef123456789abcdef12345678".to_string()), + transaction_hash: Some( + "0x56789abcdef123456789abcdef123456789abcdef123456789abcdef12345678".to_string(), + ), block_number: Some(5432109), 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(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_valuation( + 80000.0, + "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( "Registration", None, @@ -693,7 +901,7 @@ impl AssetController { Some("0x56789abcdef123456789abcdef123456789abcdef123456789abcdef12345678".to_string()), Some("Initial patent registration and tokenization".to_string()), ); - + tidal_energy_patent.add_transaction( "Licensing", Some("0x4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f".to_string()), @@ -703,9 +911,9 @@ impl AssetController { Some("0x6789abcdef123456789abcdef123456789abcdef123456789abcdef123456789".to_string()), Some("Licensing agreement with Coastal Energy Solutions".to_string()), ); - + assets.push(tidal_energy_patent); - + // Create Digital Art Artwork let mut zanzibar_heritage_nft = Asset { id: "asset-heritage-Artwork".to_string(), @@ -734,21 +942,38 @@ impl AssetController { image_url: Some("https://images.unsplash.com/photo-1519125323398-675f0ddb6308?auto=format&fit=crop&w=600&q=80".to_string()), external_url: Some("https://digitalarts.zdfz/collections/heritage/1".to_string()), }; - + zanzibar_heritage_nft.add_blockchain_info(BlockchainInfo { blockchain: "Ethereum".to_string(), token_id: "HERITAGE1".to_string(), contract_address: "0x06012c8cf97BEaD5deAe237070F9587f8E7A266d".to_string(), owner_address: "0xb794f5ea0ba39494ce839613fffba74279579268".to_string(), - transaction_hash: Some("0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234".to_string()), + transaction_hash: Some( + "0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234".to_string(), + ), block_number: Some(12345678), 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(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_valuation( + 5000.0, + "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( "Minting", None, @@ -758,7 +983,7 @@ impl AssetController { Some("0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234".to_string()), Some("Initial Artwork minting by artist".to_string()), ); - + zanzibar_heritage_nft.add_transaction( "Sale", Some("0xb794f5ea0ba39494ce839613fffba74279579268".to_string()), @@ -768,9 +993,9 @@ impl AssetController { Some("0x234567890abcdef123456789abcdef123456789abcdef123456789abcdef1234".to_string()), Some("Primary sale to ZDFZ Digital Arts Collective".to_string()), ); - + assets.push(zanzibar_heritage_nft); - + assets } } diff --git a/actix_mvc_app/src/controllers/auth.rs b/actix_mvc_app/src/controllers/auth.rs index 681a1c8..08934ba 100644 --- a/actix_mvc_app/src/controllers/auth.rs +++ b/actix_mvc_app/src/controllers/auth.rs @@ -25,6 +25,7 @@ lazy_static! { /// Controller for handling authentication-related routes pub struct AuthController; +#[allow(dead_code)] impl AuthController { /// Generate a JWT token for a user fn generate_token(email: &str, role: &UserRole) -> Result { diff --git a/actix_mvc_app/src/controllers/calendar.rs b/actix_mvc_app/src/controllers/calendar.rs index e4bfbc6..e84c55c 100644 --- a/actix_mvc_app/src/controllers/calendar.rs +++ b/actix_mvc_app/src/controllers/calendar.rs @@ -1,12 +1,17 @@ -use actix_web::{web, HttpResponse, Responder, Result}; use actix_session::Session; +use actix_web::{HttpResponse, Responder, Result, web}; use chrono::{DateTime, Datelike, NaiveDate, TimeZone, Utc}; use serde::{Deserialize, Serialize}; -use tera::Tera; use serde_json::Value; +use tera::Tera; -use crate::models::{CalendarEvent, CalendarViewMode}; -use crate::utils::{RedisCalendarService, render_template}; +use crate::db::calendar::{ + add_event_to_calendar, create_new_event, delete_event, get_events, get_or_create_user_calendar, +}; +use crate::models::CalendarViewMode; +use crate::utils::render_template; +use heromodels::models::calendar::Event; +use heromodels_core::Model; /// Controller for handling calendar-related routes pub struct CalendarController; @@ -14,9 +19,11 @@ pub struct CalendarController; impl CalendarController { /// Helper function to get user from session fn get_user_from_session(session: &Session) -> Option { - session.get::("user").ok().flatten().and_then(|user_json| { - serde_json::from_str(&user_json).ok() - }) + session + .get::("user") + .ok() + .flatten() + .and_then(|user_json| serde_json::from_str(&user_json).ok()) } /// Handles the calendar page route @@ -27,113 +34,176 @@ impl CalendarController { ) -> Result { let mut ctx = tera::Context::new(); ctx.insert("active_page", "calendar"); - + // Parse the view mode from the query parameters - let view_mode = CalendarViewMode::from_str(&query.view.clone().unwrap_or_else(|| "month".to_string())); + let view_mode = + CalendarViewMode::from_str(&query.view.clone().unwrap_or_else(|| "month".to_string())); ctx.insert("view_mode", &view_mode.to_str()); - + // Parse the date from the query parameters or use the current date let date = if let Some(date_str) = &query.date { match NaiveDate::parse_from_str(date_str, "%Y-%m-%d") { - Ok(naive_date) => Utc.from_utc_datetime(&naive_date.and_hms_opt(0, 0, 0).unwrap()).into(), + Ok(naive_date) => Utc + .from_utc_datetime(&naive_date.and_hms_opt(0, 0, 0).unwrap()) + .into(), Err(_) => Utc::now(), } } else { Utc::now() }; - + ctx.insert("current_date", &date.format("%Y-%m-%d").to_string()); ctx.insert("current_year", &date.year()); ctx.insert("current_month", &date.month()); ctx.insert("current_day", &date.day()); - - // Add user to context if available + + // Add user to context if available and ensure user has a calendar if let Some(user) = Self::get_user_from_session(&_session) { ctx.insert("user", &user); + + // Get or create user calendar + if let (Some(user_id), Some(user_name)) = ( + user.get("id").and_then(|v| v.as_u64()).map(|v| v as u32), + user.get("full_name").and_then(|v| v.as_str()), + ) { + match get_or_create_user_calendar(user_id, user_name) { + Ok(calendar) => { + log::info!( + "User calendar ready: ID {}, Name: '{}'", + calendar.get_id(), + calendar.name + ); + ctx.insert("user_calendar", &calendar); + } + Err(e) => { + log::error!("Failed to get or create user calendar: {}", e); + // Continue without calendar - the app should still work + } + } + } } - + // Get events for the current view let (start_date, end_date) = match view_mode { CalendarViewMode::Year => { let start = Utc.with_ymd_and_hms(date.year(), 1, 1, 0, 0, 0).unwrap(); - let end = Utc.with_ymd_and_hms(date.year(), 12, 31, 23, 59, 59).unwrap(); + let end = Utc + .with_ymd_and_hms(date.year(), 12, 31, 23, 59, 59) + .unwrap(); (start, end) - }, + } CalendarViewMode::Month => { - let start = Utc.with_ymd_and_hms(date.year(), date.month(), 1, 0, 0, 0).unwrap(); + let start = Utc + .with_ymd_and_hms(date.year(), date.month(), 1, 0, 0, 0) + .unwrap(); let last_day = Self::last_day_of_month(date.year(), date.month()); - let end = Utc.with_ymd_and_hms(date.year(), date.month(), last_day, 23, 59, 59).unwrap(); + let end = Utc + .with_ymd_and_hms(date.year(), date.month(), last_day, 23, 59, 59) + .unwrap(); (start, end) - }, + } CalendarViewMode::Week => { // Calculate the start of the week (Sunday) let _weekday = date.weekday().num_days_from_sunday(); - let start_date = date.date_naive().pred_opt().unwrap().pred_opt().unwrap().pred_opt().unwrap().pred_opt().unwrap().pred_opt().unwrap().pred_opt().unwrap().pred_opt().unwrap(); + let start_date = date + .date_naive() + .pred_opt() + .unwrap() + .pred_opt() + .unwrap() + .pred_opt() + .unwrap() + .pred_opt() + .unwrap() + .pred_opt() + .unwrap() + .pred_opt() + .unwrap() + .pred_opt() + .unwrap(); let start = Utc.from_utc_datetime(&start_date.and_hms_opt(0, 0, 0).unwrap()); let end = start + chrono::Duration::days(7); (start, end) - }, + } CalendarViewMode::Day => { - let start = Utc.with_ymd_and_hms(date.year(), date.month(), date.day(), 0, 0, 0).unwrap(); - let end = Utc.with_ymd_and_hms(date.year(), date.month(), date.day(), 23, 59, 59).unwrap(); + let start = Utc + .with_ymd_and_hms(date.year(), date.month(), date.day(), 0, 0, 0) + .unwrap(); + let end = Utc + .with_ymd_and_hms(date.year(), date.month(), date.day(), 23, 59, 59) + .unwrap(); (start, end) - }, + } }; - - // Get events from Redis - let events = match RedisCalendarService::get_events_in_range(start_date, end_date) { - Ok(events) => events, + + // Get events from database + let events = match get_events() { + Ok(db_events) => { + // Filter events for the date range + db_events + .into_iter() + .filter(|event| { + // Event overlaps with the date range + event.start_time < end_date && event.end_time > start_date + }) + .collect() + } Err(e) => { - log::error!("Failed to get events from Redis: {}", e); + log::error!("Failed to get events from database: {}", e); vec![] } }; - + ctx.insert("events", &events); - + // Generate calendar data based on the view mode match view_mode { CalendarViewMode::Year => { - let months = (1..=12).map(|month| { - let month_name = match month { - 1 => "January", - 2 => "February", - 3 => "March", - 4 => "April", - 5 => "May", - 6 => "June", - 7 => "July", - 8 => "August", - 9 => "September", - 10 => "October", - 11 => "November", - 12 => "December", - _ => "", - }; - - let month_events = events.iter() - .filter(|event| { - event.start_time.month() == month || event.end_time.month() == month - }) - .cloned() - .collect::>(); - - CalendarMonth { - month, - name: month_name.to_string(), - events: month_events, - } - }).collect::>(); - + let months = (1..=12) + .map(|month| { + let month_name = match month { + 1 => "January", + 2 => "February", + 3 => "March", + 4 => "April", + 5 => "May", + 6 => "June", + 7 => "July", + 8 => "August", + 9 => "September", + 10 => "October", + 11 => "November", + 12 => "December", + _ => "", + }; + + let month_events = events + .iter() + .filter(|event| { + event.start_time.month() == month || event.end_time.month() == month + }) + .cloned() + .collect::>(); + + CalendarMonth { + month, + name: month_name.to_string(), + events: month_events, + } + }) + .collect::>(); + ctx.insert("months", &months); - }, + } CalendarViewMode::Month => { let days_in_month = Self::last_day_of_month(date.year(), date.month()); - let first_day = Utc.with_ymd_and_hms(date.year(), date.month(), 1, 0, 0, 0).unwrap(); + let first_day = Utc + .with_ymd_and_hms(date.year(), date.month(), 1, 0, 0, 0) + .unwrap(); let first_weekday = first_day.weekday().num_days_from_sunday(); - + let mut calendar_days = Vec::new(); - + // Add empty days for the start of the month for _ in 0..first_weekday { calendar_days.push(CalendarDay { @@ -142,27 +212,34 @@ impl CalendarController { is_current_month: false, }); } - + // Add days for the current month for day in 1..=days_in_month { - let day_events = events.iter() + let day_events = events + .iter() .filter(|event| { - let day_start = Utc.with_ymd_and_hms(date.year(), date.month(), day, 0, 0, 0).unwrap(); - let day_end = Utc.with_ymd_and_hms(date.year(), date.month(), day, 23, 59, 59).unwrap(); - - (event.start_time <= day_end && event.end_time >= day_start) || - (event.all_day && event.start_time.day() <= day && event.end_time.day() >= day) + let day_start = Utc + .with_ymd_and_hms(date.year(), date.month(), day, 0, 0, 0) + .unwrap(); + let day_end = Utc + .with_ymd_and_hms(date.year(), date.month(), day, 23, 59, 59) + .unwrap(); + + (event.start_time <= day_end && event.end_time >= day_start) + || (event.all_day + && event.start_time.day() <= day + && event.end_time.day() >= day) }) .cloned() .collect::>(); - + calendar_days.push(CalendarDay { day, events: day_events, is_current_month: true, }); } - + // Fill out the rest of the calendar grid (6 rows of 7 days) let remaining_days = 42 - calendar_days.len(); for day in 1..=remaining_days { @@ -172,149 +249,250 @@ impl CalendarController { is_current_month: false, }); } - + ctx.insert("calendar_days", &calendar_days); ctx.insert("month_name", &Self::month_name(date.month())); - }, + } CalendarViewMode::Week => { // Calculate the start of the week (Sunday) let weekday = date.weekday().num_days_from_sunday(); let week_start = date - chrono::Duration::days(weekday as i64); - + let mut week_days = Vec::new(); for i in 0..7 { let day_date = week_start + chrono::Duration::days(i); - let day_events = events.iter() + let day_events = events + .iter() .filter(|event| { - let day_start = Utc.with_ymd_and_hms(day_date.year(), day_date.month(), day_date.day(), 0, 0, 0).unwrap(); - let day_end = Utc.with_ymd_and_hms(day_date.year(), day_date.month(), day_date.day(), 23, 59, 59).unwrap(); - - (event.start_time <= day_end && event.end_time >= day_start) || - (event.all_day && event.start_time.day() <= day_date.day() && event.end_time.day() >= day_date.day()) + let day_start = Utc + .with_ymd_and_hms( + day_date.year(), + day_date.month(), + day_date.day(), + 0, + 0, + 0, + ) + .unwrap(); + let day_end = Utc + .with_ymd_and_hms( + day_date.year(), + day_date.month(), + day_date.day(), + 23, + 59, + 59, + ) + .unwrap(); + + (event.start_time <= day_end && event.end_time >= day_start) + || (event.all_day + && event.start_time.day() <= day_date.day() + && event.end_time.day() >= day_date.day()) }) .cloned() .collect::>(); - + week_days.push(CalendarDay { day: day_date.day(), events: day_events, is_current_month: day_date.month() == date.month(), }); } - + ctx.insert("week_days", &week_days); - }, + } CalendarViewMode::Day => { log::info!("Day view selected"); - ctx.insert("day_name", &Self::day_name(date.weekday().num_days_from_sunday())); - + ctx.insert( + "day_name", + &Self::day_name(date.weekday().num_days_from_sunday()), + ); + // Add debug info log::info!("Events count: {}", events.len()); log::info!("Current date: {}", date.format("%Y-%m-%d")); - log::info!("Day name: {}", Self::day_name(date.weekday().num_days_from_sunday())); - }, + log::info!( + "Day name: {}", + Self::day_name(date.weekday().num_days_from_sunday()) + ); + } } - + render_template(&tmpl, "calendar/index.html", &ctx) } - + /// Handles the new event page route pub async fn new_event(tmpl: web::Data, _session: Session) -> Result { let mut ctx = tera::Context::new(); ctx.insert("active_page", "calendar"); - - // Add user to context if available + + // Add user to context if available and ensure user has a calendar if let Some(user) = Self::get_user_from_session(&_session) { ctx.insert("user", &user); + + // Get or create user calendar + if let (Some(user_id), Some(user_name)) = ( + user.get("id").and_then(|v| v.as_u64()).map(|v| v as u32), + user.get("full_name").and_then(|v| v.as_str()), + ) { + match get_or_create_user_calendar(user_id, user_name) { + Ok(calendar) => { + ctx.insert("user_calendar", &calendar); + } + Err(e) => { + log::error!("Failed to get or create user calendar: {}", e); + } + } + } } - + render_template(&tmpl, "calendar/new_event.html", &ctx) } - + /// Handles the create event route pub async fn create_event( form: web::Form, tmpl: web::Data, _session: Session, ) -> Result { + // Log the form data for debugging + log::info!( + "Creating event with form data: title='{}', start_time='{}', end_time='{}', all_day={}", + form.title, + form.start_time, + form.end_time, + form.all_day + ); + // Parse the start and end times let start_time = match DateTime::parse_from_rfc3339(&form.start_time) { Ok(dt) => dt.with_timezone(&Utc), Err(e) => { - log::error!("Failed to parse start time: {}", e); - return Ok(HttpResponse::BadRequest().body("Invalid start time")); + log::error!("Failed to parse start time '{}': {}", form.start_time, e); + return Ok(HttpResponse::BadRequest().body("Invalid start time format")); } }; - + let end_time = match DateTime::parse_from_rfc3339(&form.end_time) { Ok(dt) => dt.with_timezone(&Utc), Err(e) => { - log::error!("Failed to parse end time: {}", e); - return Ok(HttpResponse::BadRequest().body("Invalid end time")); + log::error!("Failed to parse end time '{}': {}", form.end_time, e); + return Ok(HttpResponse::BadRequest().body("Invalid end time format")); } }; - - // Create the event - let event = CalendarEvent::new( - form.title.clone(), - form.description.clone(), + + // Get user information from session + let user_info = Self::get_user_from_session(&_session); + let (user_id, user_name) = if let Some(user) = &user_info { + let id = user.get("id").and_then(|v| v.as_u64()).map(|v| v as u32); + let name = user + .get("full_name") + .and_then(|v| v.as_str()) + .unwrap_or("Unknown User"); + log::info!("User from session: id={:?}, name='{}'", id, name); + (id, name) + } else { + log::warn!("No user found in session"); + (None, "Unknown User") + }; + + // Create the event in the database + match create_new_event( + &form.title, + Some(&form.description), start_time, end_time, - Some(form.color.clone()), + None, // location + Some(&form.color), form.all_day, - None, // User ID would come from session in a real app - ); - - // Save the event to Redis - match RedisCalendarService::save_event(&event) { - Ok(_) => { + user_id, + None, // category + None, // reminder_minutes + ) { + Ok((event_id, _saved_event)) => { + log::info!("Created event with ID: {}", event_id); + + // If user is logged in, add the event to their calendar + if let Some(user_id) = user_id { + match get_or_create_user_calendar(user_id, user_name) { + Ok(calendar) => match add_event_to_calendar(calendar.get_id(), event_id) { + Ok(_) => { + log::info!( + "Added event {} to calendar {}", + event_id, + calendar.get_id() + ); + } + Err(e) => { + log::error!("Failed to add event to calendar: {}", e); + } + }, + Err(e) => { + log::error!("Failed to get user calendar: {}", e); + } + } + } + // Redirect to the calendar page Ok(HttpResponse::SeeOther() .append_header(("Location", "/calendar")) .finish()) - }, + } Err(e) => { - log::error!("Failed to save event to Redis: {}", e); - + log::error!("Failed to save event to database: {}", e); + // Show an error message let mut ctx = tera::Context::new(); ctx.insert("active_page", "calendar"); ctx.insert("error", "Failed to save event"); - + // Add user to context if available - if let Some(user) = Self::get_user_from_session(&_session) { + if let Some(user) = user_info { ctx.insert("user", &user); } - + let result = render_template(&tmpl, "calendar/new_event.html", &ctx)?; - - Ok(HttpResponse::InternalServerError().content_type("text/html").body(result.into_body())) + + Ok(HttpResponse::InternalServerError() + .content_type("text/html") + .body(result.into_body())) } } } - + /// Handles the delete event route pub async fn delete_event( path: web::Path, _session: Session, ) -> Result { let id = path.into_inner(); - - // Delete the event from Redis - match RedisCalendarService::delete_event(&id) { + + // Parse the event ID + let event_id = match id.parse::() { + Ok(id) => id, + Err(_) => { + log::error!("Invalid event ID: {}", id); + return Ok(HttpResponse::BadRequest().body("Invalid event ID")); + } + }; + + // Delete the event from database + match delete_event(event_id) { Ok(_) => { + log::info!("Deleted event with ID: {}", event_id); // Redirect to the calendar page Ok(HttpResponse::SeeOther() .append_header(("Location", "/calendar")) .finish()) - }, + } Err(e) => { - log::error!("Failed to delete event from Redis: {}", e); + log::error!("Failed to delete event from database: {}", e); Ok(HttpResponse::InternalServerError().body("Failed to delete event")) } } } - + /// Returns the last day of the month fn last_day_of_month(year: i32, month: u32) -> u32 { match month { @@ -326,11 +504,11 @@ impl CalendarController { } else { 28 } - }, + } _ => 30, // Default to 30 days } } - + /// Returns the name of the month fn month_name(month: u32) -> &'static str { match month { @@ -349,7 +527,7 @@ impl CalendarController { _ => "", } } - + /// Returns the name of the day fn day_name(day: u32) -> &'static str { match day { @@ -387,7 +565,7 @@ pub struct EventForm { #[derive(Debug, Serialize)] struct CalendarDay { day: u32, - events: Vec, + events: Vec, is_current_month: bool, } @@ -396,5 +574,5 @@ struct CalendarDay { struct CalendarMonth { month: u32, name: String, - events: Vec, -} \ No newline at end of file + events: Vec, +} diff --git a/actix_mvc_app/src/controllers/company.rs b/actix_mvc_app/src/controllers/company.rs index 554d1e4..cf25c3f 100644 --- a/actix_mvc_app/src/controllers/company.rs +++ b/actix_mvc_app/src/controllers/company.rs @@ -1,12 +1,12 @@ -use actix_web::{web, HttpResponse, Responder, Result}; -use actix_web::HttpRequest; -use tera::{Context, Tera}; -use serde::Deserialize; -use chrono::Utc; use crate::utils::render_template; +use actix_web::HttpRequest; +use actix_web::{HttpResponse, Result, web}; +use serde::Deserialize; +use tera::{Context, Tera}; // Form structs for company operations #[derive(Debug, Deserialize)] +#[allow(dead_code)] pub struct CompanyRegistrationForm { pub company_name: String, pub company_type: String, @@ -20,59 +20,69 @@ impl CompanyController { // Display the company management dashboard pub async fn index(tmpl: web::Data, req: HttpRequest) -> Result { let mut context = Context::new(); - + println!("DEBUG: Starting Company dashboard rendering"); - + // Add active_page for navigation highlighting context.insert("active_page", &"company"); - + // Parse query parameters let query_string = req.query_string(); - + // Check for success message if let Some(pos) = query_string.find("success=") { let start = pos + 8; // length of "success=" - let end = query_string[start..].find('&').map_or(query_string.len(), |e| e + start); + let end = query_string[start..] + .find('&') + .map_or(query_string.len(), |e| e + start); let success = &query_string[start..end]; let decoded = urlencoding::decode(success).unwrap_or_else(|_| success.into()); context.insert("success", &decoded); } - + // Check for entity context if let Some(pos) = query_string.find("entity=") { let start = pos + 7; // length of "entity=" - let end = query_string[start..].find('&').map_or(query_string.len(), |e| e + start); + let end = query_string[start..] + .find('&') + .map_or(query_string.len(), |e| e + start); let entity = &query_string[start..end]; context.insert("entity", &entity); - + // Also get entity name if present if let Some(pos) = query_string.find("entity_name=") { let start = pos + 12; // length of "entity_name=" - let end = query_string[start..].find('&').map_or(query_string.len(), |e| e + start); + let end = query_string[start..] + .find('&') + .map_or(query_string.len(), |e| e + start); let entity_name = &query_string[start..end]; - let decoded_name = urlencoding::decode(entity_name).unwrap_or_else(|_| entity_name.into()); + let decoded_name = + urlencoding::decode(entity_name).unwrap_or_else(|_| entity_name.into()); context.insert("entity_name", &decoded_name); println!("DEBUG: Entity context set to {} ({})", entity, decoded_name); } } - + println!("DEBUG: Rendering Company dashboard template"); let response = render_template(&tmpl, "company/index.html", &context); println!("DEBUG: Finished rendering Company dashboard template"); response } - + // View company details - pub async fn view_company(tmpl: web::Data, path: web::Path) -> Result { + pub async fn view_company( + tmpl: web::Data, + path: web::Path, + ) -> Result { let company_id = path.into_inner(); let mut context = Context::new(); - + println!("DEBUG: Viewing company details for {}", company_id); - + // Add active_page for navigation highlighting context.insert("active_page", &"company"); context.insert("company_id", &company_id); - + // In a real application, we would fetch company data from a database // For now, we'll use mock data based on the company_id match company_id.as_str() { @@ -85,14 +95,11 @@ impl CompanyController { context.insert("plan", &"Startup FZC - $50/month"); context.insert("next_billing", &"2025-06-01"); context.insert("payment_method", &"Credit Card (****4582)"); - + // Shareholders data - let shareholders = vec![ - ("John Smith", "60%"), - ("Sarah Johnson", "40%"), - ]; + let shareholders = vec![("John Smith", "60%"), ("Sarah Johnson", "40%")]; context.insert("shareholders", &shareholders); - + // Contracts data let contracts = vec![ ("Articles of Incorporation", "Signed"), @@ -100,7 +107,7 @@ impl CompanyController { ("Digital Asset Issuance", "Signed"), ]; context.insert("contracts", &contracts); - }, + } "company2" => { context.insert("company_name", &"Blockchain Innovations Ltd"); context.insert("company_type", &"Growth FZC"); @@ -110,7 +117,7 @@ impl CompanyController { context.insert("plan", &"Growth FZC - $100/month"); context.insert("next_billing", &"2025-06-15"); context.insert("payment_method", &"Bank Transfer"); - + // Shareholders data let shareholders = vec![ ("Michael Chen", "35%"), @@ -118,7 +125,7 @@ impl CompanyController { ("David Okonkwo", "30%"), ]; context.insert("shareholders", &shareholders); - + // Contracts data let contracts = vec![ ("Articles of Incorporation", "Signed"), @@ -127,7 +134,7 @@ impl CompanyController { ("Physical Asset Holding", "Signed"), ]; context.insert("contracts", &contracts); - }, + } "company3" => { context.insert("company_name", &"Sustainable Energy Cooperative"); context.insert("company_type", &"Cooperative FZC"); @@ -137,7 +144,7 @@ impl CompanyController { context.insert("plan", &"Cooperative FZC - $200/month"); context.insert("next_billing", &"Pending Activation"); context.insert("payment_method", &"Pending"); - + // Shareholders data let shareholders = vec![ ("Community Energy Group", "40%"), @@ -145,7 +152,7 @@ impl CompanyController { ("Sustainable Living Collective", "30%"), ]; context.insert("shareholders", &shareholders); - + // Contracts data let contracts = vec![ ("Articles of Incorporation", "Signed"), @@ -153,7 +160,7 @@ impl CompanyController { ("Cooperative Governance", "Pending"), ]; context.insert("contracts", &contracts); - }, + } _ => { // If company_id is not recognized, redirect to company index return Ok(HttpResponse::Found() @@ -161,51 +168,56 @@ impl CompanyController { .finish()); } } - + println!("DEBUG: Rendering company view template"); let response = render_template(&tmpl, "company/view.html", &context); println!("DEBUG: Finished rendering company view template"); response } - + // Switch to entity context pub async fn switch_entity(path: web::Path) -> Result { let company_id = path.into_inner(); - + println!("DEBUG: Switching to entity context for {}", company_id); - + // Get company name based on ID (in a real app, this would come from a database) let company_name = match company_id.as_str() { "company1" => "Zanzibar Digital Solutions", "company2" => "Blockchain Innovations Ltd", "company3" => "Sustainable Energy Cooperative", - _ => "Unknown Company" + _ => "Unknown Company", }; - + // In a real application, we would set a session/cookie for the current entity // Here we'll redirect back to the company page with a success message and entity parameter let success_message = format!("Switched to {} entity context", company_name); let encoded_message = urlencoding::encode(&success_message); - + Ok(HttpResponse::Found() - .append_header(("Location", format!("/company?success={}&entity={}&entity_name={}", - encoded_message, company_id, urlencoding::encode(company_name)))) + .append_header(( + "Location", + format!( + "/company?success={}&entity={}&entity_name={}", + encoded_message, + company_id, + urlencoding::encode(company_name) + ), + )) .finish()) } - + // Process company registration - pub async fn register( - mut form: actix_multipart::Multipart, - ) -> Result { - use actix_web::{http::header}; + pub async fn register(mut form: actix_multipart::Multipart) -> Result { + use actix_web::http::header; use futures_util::stream::StreamExt as _; use std::collections::HashMap; - + println!("DEBUG: Processing company registration request"); - + let mut fields: HashMap = HashMap::new(); let mut files = Vec::new(); - + // Parse multipart form while let Some(Ok(mut field)) = form.next().await { let mut value = Vec::new(); @@ -213,33 +225,47 @@ impl CompanyController { let data = chunk.unwrap(); value.extend_from_slice(&data); } - + // Get field name from content disposition let cd = field.content_disposition(); if let Some(name) = cd.get_name() { if name == "company_docs" { files.push(value); // Just collect files in memory for now } else { - fields.insert(name.to_string(), String::from_utf8_lossy(&value).to_string()); + fields.insert( + name.to_string(), + String::from_utf8_lossy(&value).to_string(), + ); } } } - + // Extract company details let company_name = fields.get("company_name").cloned().unwrap_or_default(); let company_type = fields.get("company_type").cloned().unwrap_or_default(); let shareholders = fields.get("shareholders").cloned().unwrap_or_default(); - + // Log received fields (mock DB insert) - println!("[Company Registration] Name: {}, Type: {}, Shareholders: {}, Files: {}", - company_name, company_type, shareholders, files.len()); - + println!( + "[Company Registration] Name: {}, Type: {}, Shareholders: {}, Files: {}", + company_name, + company_type, + shareholders, + files.len() + ); + // Create success message - let success_message = format!("Successfully registered {} as a {}", company_name, company_type); - + let success_message = format!( + "Successfully registered {} as a {}", + company_name, company_type + ); + // Redirect back to /company with success message Ok(HttpResponse::SeeOther() - .append_header((header::LOCATION, format!("/company?success={}", urlencoding::encode(&success_message)))) + .append_header(( + header::LOCATION, + format!("/company?success={}", urlencoding::encode(&success_message)), + )) .finish()) } } diff --git a/actix_mvc_app/src/controllers/contract.rs b/actix_mvc_app/src/controllers/contract.rs index 476d76e..9943aff 100644 --- a/actix_mvc_app/src/controllers/contract.rs +++ b/actix_mvc_app/src/controllers/contract.rs @@ -1,15 +1,18 @@ -use actix_web::{web, HttpResponse, Result, Error}; -use tera::{Context, Tera}; -use chrono::{Utc, Duration}; -use serde::Deserialize; -use serde_json::json; use actix_web::web::Query; +use actix_web::{Error, HttpResponse, Result, web}; +use chrono::{Duration, Utc}; +use serde::Deserialize; use std::collections::HashMap; +use tera::{Context, Tera}; -use crate::models::contract::{Contract, ContractStatus, ContractType, ContractStatistics, ContractSigner, ContractRevision, SignerStatus, TocItem}; +use crate::models::contract::{ + Contract, ContractRevision, ContractSigner, ContractStatistics, ContractStatus, ContractType, + SignerStatus, TocItem, +}; use crate::utils::render_template; #[derive(Debug, Deserialize)] +#[allow(dead_code)] pub struct ContractForm { pub title: String, pub description: String, @@ -18,6 +21,7 @@ pub struct ContractForm { } #[derive(Debug, Deserialize)] +#[allow(dead_code)] pub struct SignerForm { pub name: String, pub email: String, @@ -29,98 +33,99 @@ impl ContractController { // Display the contracts dashboard pub async fn index(tmpl: web::Data) -> Result { let mut context = Context::new(); - + let contracts = Self::get_mock_contracts(); let stats = ContractStatistics::new(&contracts); - + // Add active_page for navigation highlighting context.insert("active_page", &"contracts"); - + // Add stats context.insert("stats", &serde_json::to_value(stats).unwrap()); - + // Add recent contracts let recent_contracts: Vec> = contracts .iter() .take(5) .map(|c| Self::contract_to_json(c)) .collect(); - + context.insert("recent_contracts", &recent_contracts); - + // Add pending signature contracts - let pending_signature_contracts: Vec> = contracts - .iter() - .filter(|c| c.status == ContractStatus::PendingSignatures) - .map(|c| Self::contract_to_json(c)) - .collect(); - + let pending_signature_contracts: Vec> = + contracts + .iter() + .filter(|c| c.status == ContractStatus::PendingSignatures) + .map(|c| Self::contract_to_json(c)) + .collect(); + context.insert("pending_signature_contracts", &pending_signature_contracts); - + // Add draft contracts let draft_contracts: Vec> = contracts .iter() .filter(|c| c.status == ContractStatus::Draft) .map(|c| Self::contract_to_json(c)) .collect(); - + context.insert("draft_contracts", &draft_contracts); - + render_template(&tmpl, "contracts/index.html", &context) } - + // Display the list of all contracts pub async fn list(tmpl: web::Data) -> Result { let mut context = Context::new(); - + let contracts = Self::get_mock_contracts(); let contracts_data: Vec> = contracts .iter() .map(|c| Self::contract_to_json(c)) .collect(); - + // Add active_page for navigation highlighting context.insert("active_page", &"contracts"); - + context.insert("contracts", &contracts_data); context.insert("filter", &"all"); - + render_template(&tmpl, "contracts/contracts.html", &context) } - + // Display the list of user's contracts pub async fn my_contracts(tmpl: web::Data) -> Result { let mut context = Context::new(); - + let contracts = Self::get_mock_contracts(); let contracts_data: Vec> = contracts .iter() .map(|c| Self::contract_to_json(c)) .collect(); - + // Add active_page for navigation highlighting context.insert("active_page", &"contracts"); - + context.insert("contracts", &contracts_data); - + render_template(&tmpl, "contracts/my_contracts.html", &context) } - + // Display a specific contract pub async fn detail( tmpl: web::Data, path: web::Path, - query: Query> + query: Query>, ) -> Result { let contract_id = path.into_inner(); let mut context = Context::new(); - + // Add active_page for navigation highlighting context.insert("active_page", &"contracts"); - + // Find the contract by ID let contracts = Self::get_mock_contracts(); - + // For demo purposes, if the ID doesn't match exactly, just show the first contract // In a real app, we would return a 404 if the contract is not found let contract = if let Some(found) = contracts.iter().find(|c| c.id == contract_id) { @@ -129,7 +134,7 @@ impl ContractController { // For demo, just use the first contract contracts.first().unwrap() }; - + // Convert contract to JSON let contract_json = Self::contract_to_json(contract); @@ -137,10 +142,13 @@ impl ContractController { context.insert("contract", &contract_json); // If this contract uses multi-page markdown, load the selected section - println!("DEBUG: content_dir = {:?}, toc = {:?}", contract.content_dir, contract.toc); + println!( + "DEBUG: content_dir = {:?}, toc = {:?}", + contract.content_dir, contract.toc + ); if let (Some(content_dir), Some(toc)) = (&contract.content_dir, &contract.toc) { + use pulldown_cmark::{Options, Parser, html}; use std::fs; - use pulldown_cmark::{Parser, Options, html}; // Helper to flatten toc recursively fn flatten_toc<'a>(items: &'a Vec, out: &mut Vec<&'a TocItem>) { for item in items { @@ -154,15 +162,28 @@ impl ContractController { flatten_toc(&toc, &mut flat_toc); let section_param = query.get("section"); let selected_file = section_param - .and_then(|f| flat_toc.iter().find(|item| item.file == *f).map(|item| item.file.clone())) - .unwrap_or_else(|| flat_toc.get(0).map(|item| item.file.clone()).unwrap_or_default()); + .and_then(|f| { + flat_toc + .iter() + .find(|item| item.file == *f) + .map(|item| item.file.clone()) + }) + .unwrap_or_else(|| { + flat_toc + .get(0) + .map(|item| item.file.clone()) + .unwrap_or_default() + }); context.insert("section", &selected_file); let rel_path = format!("{}/{}", content_dir, selected_file); let abs_path = match std::env::current_dir() { Ok(dir) => dir.join(&rel_path), Err(_) => std::path::PathBuf::from(&rel_path), }; - println!("DEBUG: Attempting to read markdown file at absolute path: {:?}", abs_path); + println!( + "DEBUG: Attempting to read markdown file at absolute path: {:?}", + abs_path + ); match fs::read_to_string(&abs_path) { Ok(md) => { println!("DEBUG: Successfully read markdown file"); @@ -170,52 +191,63 @@ impl ContractController { let mut html_output = String::new(); html::push_html(&mut html_output, parser); context.insert("contract_section_content", &html_output); - }, + } Err(e) => { - let error_msg = format!("Error: Could not read contract section markdown at '{:?}': {}", abs_path, e); + let error_msg = format!( + "Error: Could not read contract section markdown at '{:?}': {}", + abs_path, e + ); println!("{}", error_msg); context.insert("contract_section_content_error", &error_msg); } } context.insert("toc", &toc); } - + // Count signed signers for the template - let signed_signers = contract.signers.iter().filter(|s| s.status == SignerStatus::Signed).count(); + let signed_signers = contract + .signers + .iter() + .filter(|s| s.status == SignerStatus::Signed) + .count(); context.insert("signed_signers", &signed_signers); - + // Count pending signers for the template - let pending_signers = contract.signers.iter().filter(|s| s.status == SignerStatus::Pending).count(); + let pending_signers = contract + .signers + .iter() + .filter(|s| s.status == SignerStatus::Pending) + .count(); context.insert("pending_signers", &pending_signers); - + // For demo purposes, set user_has_signed to false // In a real app, we would check if the current user has already signed context.insert("user_has_signed", &false); - + render_template(&tmpl, "contracts/contract_detail.html", &context) } - + // Display the create contract form pub async fn create_form(tmpl: web::Data) -> Result { let mut context = Context::new(); - + // Add active_page for navigation highlighting context.insert("active_page", &"contracts"); - + // Add contract types for dropdown let contract_types = vec![ ("Service", "Service Agreement"), ("Employment", "Employment Contract"), ("NDA", "Non-Disclosure Agreement"), ("SLA", "Service Level Agreement"), - ("Other", "Other") + ("Other", "Other"), ]; - + context.insert("contract_types", &contract_types); - + render_template(&tmpl, "contracts/create_contract.html", &context) } - + // Process the create contract form pub async fn create( _tmpl: web::Data, @@ -223,158 +255,334 @@ impl ContractController { ) -> Result { // In a real application, we would save the contract to the database // For now, we'll just redirect to the contracts list - - Ok(HttpResponse::Found().append_header(("Location", "/contracts")).finish()) + + Ok(HttpResponse::Found() + .append_header(("Location", "/contracts")) + .finish()) } - + // Helper method to convert Contract to a JSON object for templates fn contract_to_json(contract: &Contract) -> serde_json::Map { let mut map = serde_json::Map::new(); - + // Basic contract info - map.insert("id".to_string(), serde_json::Value::String(contract.id.clone())); - map.insert("title".to_string(), serde_json::Value::String(contract.title.clone())); - map.insert("description".to_string(), serde_json::Value::String(contract.description.clone())); - map.insert("status".to_string(), serde_json::Value::String(contract.status.as_str().to_string())); - map.insert("contract_type".to_string(), serde_json::Value::String(contract.contract_type.as_str().to_string())); - map.insert("created_by".to_string(), serde_json::Value::String(contract.created_by.clone())); - map.insert("created_at".to_string(), serde_json::Value::String(contract.created_at.format("%Y-%m-%d").to_string())); - map.insert("updated_at".to_string(), serde_json::Value::String(contract.updated_at.format("%Y-%m-%d").to_string())); - + map.insert( + "id".to_string(), + serde_json::Value::String(contract.id.clone()), + ); + map.insert( + "title".to_string(), + serde_json::Value::String(contract.title.clone()), + ); + map.insert( + "description".to_string(), + serde_json::Value::String(contract.description.clone()), + ); + map.insert( + "status".to_string(), + serde_json::Value::String(contract.status.as_str().to_string()), + ); + map.insert( + "contract_type".to_string(), + serde_json::Value::String(contract.contract_type.as_str().to_string()), + ); + map.insert( + "created_by".to_string(), + serde_json::Value::String(contract.created_by.clone()), + ); + map.insert( + "created_at".to_string(), + serde_json::Value::String(contract.created_at.format("%Y-%m-%d").to_string()), + ); + map.insert( + "updated_at".to_string(), + serde_json::Value::String(contract.updated_at.format("%Y-%m-%d").to_string()), + ); + // Organization info if let Some(org) = &contract.organization_id { - map.insert("organization".to_string(), serde_json::Value::String(org.clone())); + map.insert( + "organization".to_string(), + serde_json::Value::String(org.clone()), + ); } else { map.insert("organization".to_string(), serde_json::Value::Null); } - + // Add signers - let signers: Vec = contract.signers.iter() + let signers: Vec = contract + .signers + .iter() .map(|s| { let mut signer_map = serde_json::Map::new(); signer_map.insert("id".to_string(), serde_json::Value::String(s.id.clone())); - signer_map.insert("name".to_string(), serde_json::Value::String(s.name.clone())); - signer_map.insert("email".to_string(), serde_json::Value::String(s.email.clone())); - signer_map.insert("status".to_string(), serde_json::Value::String(s.status.as_str().to_string())); - + signer_map.insert( + "name".to_string(), + serde_json::Value::String(s.name.clone()), + ); + signer_map.insert( + "email".to_string(), + serde_json::Value::String(s.email.clone()), + ); + signer_map.insert( + "status".to_string(), + serde_json::Value::String(s.status.as_str().to_string()), + ); + if let Some(signed_at) = s.signed_at { - signer_map.insert("signed_at".to_string(), serde_json::Value::String(signed_at.format("%Y-%m-%d").to_string())); + signer_map.insert( + "signed_at".to_string(), + serde_json::Value::String(signed_at.format("%Y-%m-%d").to_string()), + ); } else { // For display purposes, add a placeholder date for pending signers if s.status == SignerStatus::Pending { - signer_map.insert("signed_at".to_string(), serde_json::Value::String("Pending".to_string())); + signer_map.insert( + "signed_at".to_string(), + serde_json::Value::String("Pending".to_string()), + ); } else if s.status == SignerStatus::Rejected { - signer_map.insert("signed_at".to_string(), serde_json::Value::String("Rejected".to_string())); + signer_map.insert( + "signed_at".to_string(), + serde_json::Value::String("Rejected".to_string()), + ); } } - + if let Some(comments) = &s.comments { - signer_map.insert("comments".to_string(), serde_json::Value::String(comments.clone())); + signer_map.insert( + "comments".to_string(), + serde_json::Value::String(comments.clone()), + ); } else { - signer_map.insert("comments".to_string(), serde_json::Value::String("".to_string())); + signer_map.insert( + "comments".to_string(), + serde_json::Value::String("".to_string()), + ); } - + serde_json::Value::Object(signer_map) }) .collect(); - + map.insert("signers".to_string(), serde_json::Value::Array(signers)); - + // Add pending_signers count for templates - let pending_signers = contract.signers.iter().filter(|s| s.status == SignerStatus::Pending).count(); - map.insert("pending_signers".to_string(), serde_json::Value::Number(serde_json::Number::from(pending_signers))); - + let pending_signers = contract + .signers + .iter() + .filter(|s| s.status == SignerStatus::Pending) + .count(); + map.insert( + "pending_signers".to_string(), + serde_json::Value::Number(serde_json::Number::from(pending_signers)), + ); + // Add signed_signers count for templates - let signed_signers = contract.signers.iter().filter(|s| s.status == SignerStatus::Signed).count(); - map.insert("signed_signers".to_string(), serde_json::Value::Number(serde_json::Number::from(signed_signers))); - + let signed_signers = contract + .signers + .iter() + .filter(|s| s.status == SignerStatus::Signed) + .count(); + map.insert( + "signed_signers".to_string(), + serde_json::Value::Number(serde_json::Number::from(signed_signers)), + ); + // Add revisions - let revisions: Vec = contract.revisions.iter() + let revisions: Vec = contract + .revisions + .iter() .map(|r| { let mut revision_map = serde_json::Map::new(); - revision_map.insert("version".to_string(), serde_json::Value::Number(serde_json::Number::from(r.version))); - revision_map.insert("content".to_string(), serde_json::Value::String(r.content.clone())); - revision_map.insert("created_at".to_string(), serde_json::Value::String(r.created_at.format("%Y-%m-%d").to_string())); - revision_map.insert("created_by".to_string(), serde_json::Value::String(r.created_by.clone())); - + revision_map.insert( + "version".to_string(), + serde_json::Value::Number(serde_json::Number::from(r.version)), + ); + revision_map.insert( + "content".to_string(), + serde_json::Value::String(r.content.clone()), + ); + revision_map.insert( + "created_at".to_string(), + serde_json::Value::String(r.created_at.format("%Y-%m-%d").to_string()), + ); + revision_map.insert( + "created_by".to_string(), + serde_json::Value::String(r.created_by.clone()), + ); + if let Some(comments) = &r.comments { - revision_map.insert("comments".to_string(), serde_json::Value::String(comments.clone())); + revision_map.insert( + "comments".to_string(), + serde_json::Value::String(comments.clone()), + ); // Add notes field using comments since ContractRevision doesn't have a notes field - revision_map.insert("notes".to_string(), serde_json::Value::String(comments.clone())); + revision_map.insert( + "notes".to_string(), + serde_json::Value::String(comments.clone()), + ); } else { - revision_map.insert("comments".to_string(), serde_json::Value::String("".to_string())); - revision_map.insert("notes".to_string(), serde_json::Value::String("".to_string())); + revision_map.insert( + "comments".to_string(), + serde_json::Value::String("".to_string()), + ); + revision_map.insert( + "notes".to_string(), + serde_json::Value::String("".to_string()), + ); } - + serde_json::Value::Object(revision_map) }) .collect(); - - map.insert("revisions".to_string(), serde_json::Value::Array(revisions.clone())); - + + map.insert( + "revisions".to_string(), + serde_json::Value::Array(revisions.clone()), + ); + // Add current_version - map.insert("current_version".to_string(), serde_json::Value::Number(serde_json::Number::from(contract.current_version))); - + map.insert( + "current_version".to_string(), + serde_json::Value::Number(serde_json::Number::from(contract.current_version)), + ); + // Add latest_revision as an object if !contract.revisions.is_empty() { // Find the latest revision based on version number if let Some(latest) = contract.revisions.iter().max_by_key(|r| r.version) { let mut latest_revision_map = serde_json::Map::new(); - latest_revision_map.insert("version".to_string(), serde_json::Value::Number(serde_json::Number::from(latest.version))); - latest_revision_map.insert("content".to_string(), serde_json::Value::String(latest.content.clone())); - latest_revision_map.insert("created_at".to_string(), serde_json::Value::String(latest.created_at.format("%Y-%m-%d").to_string())); - latest_revision_map.insert("created_by".to_string(), serde_json::Value::String(latest.created_by.clone())); - + latest_revision_map.insert( + "version".to_string(), + serde_json::Value::Number(serde_json::Number::from(latest.version)), + ); + latest_revision_map.insert( + "content".to_string(), + serde_json::Value::String(latest.content.clone()), + ); + latest_revision_map.insert( + "created_at".to_string(), + serde_json::Value::String(latest.created_at.format("%Y-%m-%d").to_string()), + ); + latest_revision_map.insert( + "created_by".to_string(), + serde_json::Value::String(latest.created_by.clone()), + ); + if let Some(comments) = &latest.comments { - latest_revision_map.insert("comments".to_string(), serde_json::Value::String(comments.clone())); - latest_revision_map.insert("notes".to_string(), serde_json::Value::String(comments.clone())); + latest_revision_map.insert( + "comments".to_string(), + serde_json::Value::String(comments.clone()), + ); + latest_revision_map.insert( + "notes".to_string(), + serde_json::Value::String(comments.clone()), + ); } else { - latest_revision_map.insert("comments".to_string(), serde_json::Value::String("".to_string())); - latest_revision_map.insert("notes".to_string(), serde_json::Value::String("".to_string())); + latest_revision_map.insert( + "comments".to_string(), + serde_json::Value::String("".to_string()), + ); + latest_revision_map.insert( + "notes".to_string(), + serde_json::Value::String("".to_string()), + ); } - - map.insert("latest_revision".to_string(), serde_json::Value::Object(latest_revision_map)); + + map.insert( + "latest_revision".to_string(), + serde_json::Value::Object(latest_revision_map), + ); } else { // Create an empty latest_revision object to avoid template errors let mut empty_revision = serde_json::Map::new(); - empty_revision.insert("version".to_string(), serde_json::Value::Number(serde_json::Number::from(0))); - empty_revision.insert("content".to_string(), serde_json::Value::String("No content available".to_string())); - empty_revision.insert("created_at".to_string(), serde_json::Value::String("N/A".to_string())); - empty_revision.insert("created_by".to_string(), serde_json::Value::String("N/A".to_string())); - empty_revision.insert("comments".to_string(), serde_json::Value::String("".to_string())); - empty_revision.insert("notes".to_string(), serde_json::Value::String("".to_string())); - - map.insert("latest_revision".to_string(), serde_json::Value::Object(empty_revision)); + empty_revision.insert( + "version".to_string(), + serde_json::Value::Number(serde_json::Number::from(0)), + ); + empty_revision.insert( + "content".to_string(), + serde_json::Value::String("No content available".to_string()), + ); + empty_revision.insert( + "created_at".to_string(), + serde_json::Value::String("N/A".to_string()), + ); + empty_revision.insert( + "created_by".to_string(), + serde_json::Value::String("N/A".to_string()), + ); + empty_revision.insert( + "comments".to_string(), + serde_json::Value::String("".to_string()), + ); + empty_revision.insert( + "notes".to_string(), + serde_json::Value::String("".to_string()), + ); + + map.insert( + "latest_revision".to_string(), + serde_json::Value::Object(empty_revision), + ); } } else { // Create an empty latest_revision object to avoid template errors let mut empty_revision = serde_json::Map::new(); - empty_revision.insert("version".to_string(), serde_json::Value::Number(serde_json::Number::from(0))); - empty_revision.insert("content".to_string(), serde_json::Value::String("No content available".to_string())); - empty_revision.insert("created_at".to_string(), serde_json::Value::String("N/A".to_string())); - empty_revision.insert("created_by".to_string(), serde_json::Value::String("N/A".to_string())); - empty_revision.insert("comments".to_string(), serde_json::Value::String("".to_string())); - empty_revision.insert("notes".to_string(), serde_json::Value::String("".to_string())); - - map.insert("latest_revision".to_string(), serde_json::Value::Object(empty_revision)); + empty_revision.insert( + "version".to_string(), + serde_json::Value::Number(serde_json::Number::from(0)), + ); + empty_revision.insert( + "content".to_string(), + serde_json::Value::String("No content available".to_string()), + ); + empty_revision.insert( + "created_at".to_string(), + serde_json::Value::String("N/A".to_string()), + ); + empty_revision.insert( + "created_by".to_string(), + serde_json::Value::String("N/A".to_string()), + ); + empty_revision.insert( + "comments".to_string(), + serde_json::Value::String("".to_string()), + ); + empty_revision.insert( + "notes".to_string(), + serde_json::Value::String("".to_string()), + ); + + map.insert( + "latest_revision".to_string(), + serde_json::Value::Object(empty_revision), + ); } - + // Add effective and expiration dates if present if let Some(effective_date) = &contract.effective_date { - map.insert("effective_date".to_string(), serde_json::Value::String(effective_date.format("%Y-%m-%d").to_string())); + map.insert( + "effective_date".to_string(), + serde_json::Value::String(effective_date.format("%Y-%m-%d").to_string()), + ); } - + if let Some(expiration_date) = &contract.expiration_date { - map.insert("expiration_date".to_string(), serde_json::Value::String(expiration_date.format("%Y-%m-%d").to_string())); + map.insert( + "expiration_date".to_string(), + serde_json::Value::String(expiration_date.format("%Y-%m-%d").to_string()), + ); } - + map } - + // Generate mock contracts for testing fn get_mock_contracts() -> Vec { let mut contracts = Vec::new(); - + // Mock contract 1 - Signed Service Agreement let mut contract1 = Contract { content_dir: None, @@ -394,7 +602,7 @@ impl ContractController { revisions: Vec::new(), current_version: 2, }; - + // Add signers to contract 1 contract1.signers.push(ContractSigner { id: "signer-001".to_string(), @@ -404,7 +612,7 @@ impl ContractController { signed_at: Some(Utc::now() - Duration::days(5)), comments: Some("Approved as per our discussion.".to_string()), }); - + contract1.signers.push(ContractSigner { id: "signer-002".to_string(), name: "Nala Okafor".to_string(), @@ -413,7 +621,7 @@ impl ContractController { signed_at: Some(Utc::now() - Duration::days(6)), comments: Some("Terms look good. Happy to proceed.".to_string()), }); - + // Add revisions to contract 1 contract1.revisions.push(ContractRevision { version: 1, @@ -422,7 +630,7 @@ impl ContractController { created_by: "Wei Chen".to_string(), comments: Some("Initial draft of the service agreement.".to_string()), }); - + contract1.revisions.push(ContractRevision { version: 2, content: "

Digital Hub Service Agreement

This Service Agreement (the \"Agreement\") is entered into between Zanzibar Digital Hub (\"Provider\") and the undersigned client (\"Client\").

1. Services

Provider agrees to provide Client with cloud hosting and digital infrastructure services as specified in Appendix A.

2. Term

This Agreement shall commence on the Effective Date and continue for a period of one (1) year unless terminated earlier in accordance with the terms herein.

3. Fees

Client agrees to pay Provider the fees set forth in Appendix B. All fees are due within thirty (30) days of invoice date.

4. Confidentiality

Each party agrees to maintain the confidentiality of any proprietary information received from the other party during the term of this Agreement.

5. Data Protection

Provider shall implement appropriate technical and organizational measures to ensure a level of security appropriate to the risk, including encryption of personal data, and shall comply with all applicable data protection laws.

".to_string(), @@ -430,7 +638,7 @@ impl ContractController { created_by: "Wei Chen".to_string(), comments: Some("Added data protection clause as requested by legal.".to_string()), }); - + // Mock contract 2 - Pending Signatures let mut contract2 = Contract { content_dir: None, @@ -450,7 +658,7 @@ impl ContractController { revisions: Vec::new(), current_version: 1, }; - + // Add signers to contract 2 contract2.signers.push(ContractSigner { id: "signer-003".to_string(), @@ -460,7 +668,7 @@ impl ContractController { signed_at: Some(Utc::now() - Duration::days(2)), comments: None, }); - + contract2.signers.push(ContractSigner { id: "signer-004".to_string(), name: "Maya Rodriguez".to_string(), @@ -469,7 +677,7 @@ impl ContractController { signed_at: None, comments: None, }); - + contract2.signers.push(ContractSigner { id: "signer-005".to_string(), name: "Jamal Washington".to_string(), @@ -478,7 +686,7 @@ impl ContractController { signed_at: None, comments: None, }); - + // Add revisions to contract 2 contract2.revisions.push(ContractRevision { version: 1, @@ -487,7 +695,7 @@ impl ContractController { created_by: "Dr. Raj Patel".to_string(), comments: Some("Initial draft of the development agreement.".to_string()), }); - + // Mock contract 3 - Draft let mut contract3 = Contract { id: "contract-003".to_string(), @@ -554,7 +762,6 @@ impl ContractController { ]), }; - // Add potential signers to contract 3 (still in draft) contract3.signers.push(ContractSigner { id: "signer-006".to_string(), @@ -564,7 +771,7 @@ impl ContractController { signed_at: None, comments: None, }); - + contract3.signers.push(ContractSigner { id: "signer-007".to_string(), name: "Ibrahim Al-Farsi".to_string(), @@ -573,59 +780,57 @@ impl ContractController { signed_at: None, comments: None, }); - + // Add ToC and content directory to contract 3 contract3.content_dir = Some("src/content/contract-003".to_string()); - contract3.toc = Some(vec![ - TocItem { - title: "Digital Asset Tokenization Agreement".to_string(), - file: "cover.md".to_string(), - children: vec![ - TocItem { - title: "1. Purpose".to_string(), - file: "1-purpose.md".to_string(), - children: vec![], - }, - TocItem { - title: "2. Tokenization Process".to_string(), - file: "2-tokenization-process.md".to_string(), - children: vec![], - }, - TocItem { - title: "3. Revenue Sharing".to_string(), - file: "3-revenue-sharing.md".to_string(), - children: vec![], - }, - TocItem { - title: "4. Governance".to_string(), - file: "4-governance.md".to_string(), - children: vec![], - }, - TocItem { - title: "Appendix A: Properties".to_string(), - file: "appendix-a.md".to_string(), - children: vec![], - }, - TocItem { - title: "Appendix B: Specifications".to_string(), - file: "appendix-b.md".to_string(), - children: vec![], - }, - TocItem { - title: "Appendix C: Revenue Formula".to_string(), - file: "appendix-c.md".to_string(), - children: vec![], - }, - TocItem { - title: "Appendix D: Governance Framework".to_string(), - file: "appendix-d.md".to_string(), - children: vec![], - }, - ], - } - ]); + contract3.toc = Some(vec![TocItem { + title: "Digital Asset Tokenization Agreement".to_string(), + file: "cover.md".to_string(), + children: vec![ + TocItem { + title: "1. Purpose".to_string(), + file: "1-purpose.md".to_string(), + children: vec![], + }, + TocItem { + title: "2. Tokenization Process".to_string(), + file: "2-tokenization-process.md".to_string(), + children: vec![], + }, + TocItem { + title: "3. Revenue Sharing".to_string(), + file: "3-revenue-sharing.md".to_string(), + children: vec![], + }, + TocItem { + title: "4. Governance".to_string(), + file: "4-governance.md".to_string(), + children: vec![], + }, + TocItem { + title: "Appendix A: Properties".to_string(), + file: "appendix-a.md".to_string(), + children: vec![], + }, + TocItem { + title: "Appendix B: Specifications".to_string(), + file: "appendix-b.md".to_string(), + children: vec![], + }, + TocItem { + title: "Appendix C: Revenue Formula".to_string(), + file: "appendix-c.md".to_string(), + children: vec![], + }, + TocItem { + title: "Appendix D: Governance Framework".to_string(), + file: "appendix-d.md".to_string(), + children: vec![], + }, + ], + }]); // No revision content for contract 3, content is in markdown files. - + // Mock contract 4 - Rejected let mut contract4 = Contract { content_dir: None, @@ -645,7 +850,7 @@ impl ContractController { revisions: Vec::new(), current_version: 1, }; - + // Add signers to contract 4 with a rejection contract4.signers.push(ContractSigner { id: "signer-008".to_string(), @@ -655,7 +860,7 @@ impl ContractController { signed_at: Some(Utc::now() - Duration::days(10)), comments: None, }); - + contract4.signers.push(ContractSigner { id: "signer-009".to_string(), name: "Dr. Amina Diallo".to_string(), @@ -664,7 +869,7 @@ impl ContractController { signed_at: Some(Utc::now() - Duration::days(8)), comments: Some("Cannot agree to these terms due to privacy concerns. Please revise section 3.2 regarding data retention.".to_string()), }); - + // Add revisions to contract 4 contract4.revisions.push(ContractRevision { version: 1, @@ -673,7 +878,7 @@ impl ContractController { created_by: "Wei Chen".to_string(), comments: Some("Initial draft of the data sharing agreement.".to_string()), }); - + // Mock contract 5 - Active let mut contract5 = Contract { content_dir: None, @@ -693,7 +898,7 @@ impl ContractController { revisions: Vec::new(), current_version: 2, }; - + // Add signers to contract 5 contract5.signers.push(ContractSigner { id: "signer-010".to_string(), @@ -703,7 +908,7 @@ impl ContractController { signed_at: Some(Utc::now() - Duration::days(47)), comments: None, }); - + contract5.signers.push(ContractSigner { id: "signer-011".to_string(), name: "Li Wei".to_string(), @@ -712,7 +917,7 @@ impl ContractController { signed_at: Some(Utc::now() - Duration::days(45)), comments: Some("Approved after legal review.".to_string()), }); - + // Add revisions to contract 5 contract5.revisions.push(ContractRevision { version: 1, @@ -721,7 +926,7 @@ impl ContractController { created_by: "Maya Rodriguez".to_string(), comments: Some("Initial draft of the identity verification service agreement.".to_string()), }); - + contract5.revisions.push(ContractRevision { version: 2, content: "

Digital Identity Verification Service Agreement

This Service Agreement (the \"Agreement\") is entered into between Zanzibar Digital Hub (\"Provider\") and the businesses listed in Appendix A (\"Clients\").

1. Services

Provider agrees to provide Clients with digital identity verification services as specified in Appendix B.

2. Term

This Agreement shall commence on the Effective Date and continue for a period of one (1) year unless terminated earlier in accordance with the terms herein.

3. Fees

Clients agree to pay Provider the fees set forth in Appendix C. All fees are due within thirty (30) days of invoice date.

4. Service Level Agreement

Provider shall maintain a service uptime of at least 99.9% as measured on a monthly basis.

5. Compliance

Provider shall comply with all applicable laws and regulations regarding identity verification and data protection, including but not limited to the Zanzibar Digital Economy Act.

".to_string(), @@ -729,14 +934,14 @@ impl ContractController { created_by: "Maya Rodriguez".to_string(), comments: Some("Added compliance clause as requested by legal.".to_string()), }); - + // Add all contracts to the vector contracts.push(contract1); contracts.push(contract2); contracts.push(contract3); contracts.push(contract4); contracts.push(contract5); - + contracts } } diff --git a/actix_mvc_app/src/controllers/defi.rs b/actix_mvc_app/src/controllers/defi.rs index ec74d0b..7a275ad 100644 --- a/actix_mvc_app/src/controllers/defi.rs +++ b/actix_mvc_app/src/controllers/defi.rs @@ -1,12 +1,15 @@ -use actix_web::{web, HttpResponse, Result}; use actix_web::HttpRequest; -use tera::{Context, Tera}; -use chrono::{Utc, Duration}; +use actix_web::{HttpResponse, Result, web}; +use chrono::{Duration, Utc}; use serde::Deserialize; +use tera::{Context, Tera}; use uuid::Uuid; -use crate::models::asset::{Asset, AssetType, AssetStatus}; -use crate::models::defi::{DefiPosition, DefiPositionType, DefiPositionStatus, ProvidingPosition, ReceivingPosition, DEFI_DB}; +use crate::models::asset::Asset; +use crate::models::defi::{ + DEFI_DB, DefiPosition, DefiPositionStatus, DefiPositionType, ProvidingPosition, + ReceivingPosition, +}; use crate::utils::render_template; // Form structs for DeFi operations @@ -26,6 +29,7 @@ pub struct ReceivingForm { } #[derive(Debug, Deserialize)] +#[allow(dead_code)] pub struct LiquidityForm { pub first_token: String, pub first_amount: f64, @@ -35,6 +39,7 @@ pub struct LiquidityForm { } #[derive(Debug, Deserialize)] +#[allow(dead_code)] pub struct StakingForm { pub asset_id: String, pub amount: f64, @@ -49,6 +54,7 @@ pub struct SwapForm { } #[derive(Debug, Deserialize)] +#[allow(dead_code)] pub struct CollateralForm { pub asset_id: String, pub amount: f64, @@ -63,29 +69,29 @@ impl DefiController { // Display the DeFi dashboard pub async fn index(tmpl: web::Data, req: HttpRequest) -> Result { let mut context = Context::new(); - + println!("DEBUG: Starting DeFi dashboard rendering"); - + // Get mock assets for the dropdown selectors let assets = Self::get_mock_assets(); println!("DEBUG: Generated {} mock assets", assets.len()); - + // Add active_page for navigation highlighting context.insert("active_page", &"defi"); - + // Add DeFi stats let defi_stats = Self::get_defi_stats(); context.insert("defi_stats", &serde_json::to_value(defi_stats).unwrap()); - + // Add recent assets for selection in forms let recent_assets: Vec> = assets .iter() .take(5) .map(|a| Self::asset_to_json(a)) .collect(); - + context.insert("recent_assets", &recent_assets); - + // Get user's providing positions let db = DEFI_DB.lock().unwrap(); let providing_positions = db.get_user_providing_positions("user123"); @@ -94,7 +100,7 @@ impl DefiController { .map(|p| serde_json::to_value(p).unwrap()) .collect(); context.insert("providing_positions", &providing_positions_json); - + // Get user's receiving positions let receiving_positions = db.get_user_receiving_positions("user123"); let receiving_positions_json: Vec = receiving_positions @@ -102,27 +108,30 @@ impl DefiController { .map(|p| serde_json::to_value(p).unwrap()) .collect(); context.insert("receiving_positions", &receiving_positions_json); - + // Add success message if present in query params if let Some(success) = req.query_string().strip_prefix("success=") { let decoded = urlencoding::decode(success).unwrap_or_else(|_| success.into()); context.insert("success_message", &decoded); } - + println!("DEBUG: Rendering DeFi dashboard template"); let response = render_template(&tmpl, "defi/index.html", &context); println!("DEBUG: Finished rendering DeFi dashboard template"); response } - + // Process providing request - pub async fn create_providing(_tmpl: web::Data, form: web::Form) -> Result { + pub async fn create_providing( + _tmpl: web::Data, + form: web::Form, + ) -> Result { println!("DEBUG: Processing providing request: {:?}", form); - + // Get the asset obligationails (in a real app, this would come from a database) let assets = Self::get_mock_assets(); let asset = assets.iter().find(|a| a.id == form.asset_id); - + if let Some(asset) = asset { // Calculate profit share and return amount let profit_share = match form.duration { @@ -133,9 +142,10 @@ impl DefiController { 365 => 12.0, _ => 4.2, // Default to 30 days rate }; - - let return_amount = form.amount + (form.amount * (profit_share / 100.0) * (form.duration as f64 / 365.0)); - + + let return_amount = form.amount + + (form.amount * (profit_share / 100.0) * (form.duration as f64 / 365.0)); + // Create a new providing position let providing_position = ProvidingPosition { base: DefiPosition { @@ -156,17 +166,23 @@ impl DefiController { profit_share_earned: profit_share, return_amount, }; - + // Add the position to the database { let mut db = DEFI_DB.lock().unwrap(); db.add_providing_position(providing_position); } - + // Redirect with success message - let success_message = format!("Successfully provided {} {} for {} days", form.amount, asset.name, form.duration); + let success_message = format!( + "Successfully provided {} {} for {} days", + form.amount, asset.name, form.duration + ); Ok(HttpResponse::SeeOther() - .append_header(("Location", format!("/defi?success={}", urlencoding::encode(&success_message)))) + .append_header(( + "Location", + format!("/defi?success={}", urlencoding::encode(&success_message)), + )) .finish()) } else { // Asset not found, redirect with error @@ -175,15 +191,18 @@ impl DefiController { .finish()) } } - + // Process receiving request - pub async fn create_receiving(_tmpl: web::Data, form: web::Form) -> Result { + pub async fn create_receiving( + _tmpl: web::Data, + form: web::Form, + ) -> Result { println!("DEBUG: Processing receiving request: {:?}", form); - + // Get the asset obligationails (in a real app, this would come from a database) let assets = Self::get_mock_assets(); let collateral_asset = assets.iter().find(|a| a.id == form.collateral_asset_id); - + if let Some(collateral_asset) = collateral_asset { // Calculate profit share rate based on duration let profit_share_rate = match form.duration { @@ -194,15 +213,17 @@ impl DefiController { 365 => 10.0, _ => 5.0, // Default to 30 days rate }; - + // Calculate profit share and total to repay - let profit_share = form.amount * (profit_share_rate / 100.0) * (form.duration as f64 / 365.0); + let profit_share = + form.amount * (profit_share_rate / 100.0) * (form.duration as f64 / 365.0); let total_to_repay = form.amount + profit_share; - + // Calculate collateral value and ratio - let collateral_value = form.collateral_amount * collateral_asset.latest_valuation().map_or(0.5, |v| v.value); + let collateral_value = form.collateral_amount + * collateral_asset.latest_valuation().map_or(0.5, |v| v.value); let collateral_ratio = (collateral_value / form.amount) * 100.0; - + // Create a new receiving position let receiving_position = ReceivingPosition { base: DefiPosition { @@ -230,18 +251,23 @@ impl DefiController { total_to_repay, collateral_ratio, }; - + // Add the position to the database { let mut db = DEFI_DB.lock().unwrap(); db.add_receiving_position(receiving_position); } - + // Redirect with success message - let success_message = format!("Successfully borrowed {} ZDFZ using {} {} as collateral", - form.amount, form.collateral_amount, collateral_asset.name); + let success_message = format!( + "Successfully borrowed {} ZDFZ using {} {} as collateral", + form.amount, form.collateral_amount, collateral_asset.name + ); Ok(HttpResponse::SeeOther() - .append_header(("Location", format!("/defi?success={}", urlencoding::encode(&success_message)))) + .append_header(( + "Location", + format!("/defi?success={}", urlencoding::encode(&success_message)), + )) .finish()) } else { // Asset not found, redirect with error @@ -250,116 +276,202 @@ impl DefiController { .finish()) } } - + // Process liquidity provision - pub async fn add_liquidity(_tmpl: web::Data, form: web::Form) -> Result { + pub async fn add_liquidity( + _tmpl: web::Data, + form: web::Form, + ) -> Result { println!("DEBUG: Processing liquidity provision: {:?}", form); - + // In a real application, this would add liquidity to a pool in the database // For now, we'll just redirect back to the DeFi dashboard with a success message - - let success_message = format!("Successfully added liquidity: {} {} and {} {}", - form.first_amount, form.first_token, form.second_amount, form.second_token); - + + let success_message = format!( + "Successfully added liquidity: {} {} and {} {}", + form.first_amount, form.first_token, form.second_amount, form.second_token + ); + Ok(HttpResponse::SeeOther() - .append_header(("Location", format!("/defi?success={}", urlencoding::encode(&success_message)))) + .append_header(( + "Location", + format!("/defi?success={}", urlencoding::encode(&success_message)), + )) .finish()) } - + // Process staking request - pub async fn create_staking(_tmpl: web::Data, form: web::Form) -> Result { + pub async fn create_staking( + _tmpl: web::Data, + form: web::Form, + ) -> Result { println!("DEBUG: Processing staking request: {:?}", form); - + // In a real application, this would create a staking position in the database // For now, we'll just redirect back to the DeFi dashboard with a success message - + let success_message = format!("Successfully staked {} {}", form.amount, form.asset_id); - + Ok(HttpResponse::SeeOther() - .append_header(("Location", format!("/defi?success={}", urlencoding::encode(&success_message)))) + .append_header(( + "Location", + format!("/defi?success={}", urlencoding::encode(&success_message)), + )) .finish()) } - + // Process token swap - pub async fn swap_tokens(_tmpl: web::Data, form: web::Form) -> Result { + pub async fn swap_tokens( + _tmpl: web::Data, + form: web::Form, + ) -> Result { println!("DEBUG: Processing token swap: {:?}", form); - + // In a real application, this would perform a token swap in the database // For now, we'll just redirect back to the DeFi dashboard with a success message - - let success_message = format!("Successfully swapped {} {} to {}", - form.from_amount, form.from_token, form.to_token); - + + let success_message = format!( + "Successfully swapped {} {} to {}", + form.from_amount, form.from_token, form.to_token + ); + Ok(HttpResponse::SeeOther() - .append_header(("Location", format!("/defi?success={}", urlencoding::encode(&success_message)))) + .append_header(( + "Location", + format!("/defi?success={}", urlencoding::encode(&success_message)), + )) .finish()) } - + // Process collateral position creation - pub async fn create_collateral(_tmpl: web::Data, form: web::Form) -> Result { + pub async fn create_collateral( + _tmpl: web::Data, + form: web::Form, + ) -> Result { println!("DEBUG: Processing collateral creation: {:?}", form); - + // In a real application, this would create a collateral position in the database // For now, we'll just redirect back to the DeFi dashboard with a success message - + let purpose_str = match form.purpose.as_str() { "funds" => "secure a funds", "synthetic" => "generate synthetic assets", "leverage" => "leverage trading", _ => "collateralization", }; - - let success_message = format!("Successfully collateralized {} {} for {}", - form.amount, form.asset_id, purpose_str); - + + let success_message = format!( + "Successfully collateralized {} {} for {}", + form.amount, form.asset_id, purpose_str + ); + Ok(HttpResponse::SeeOther() - .append_header(("Location", format!("/defi?success={}", urlencoding::encode(&success_message)))) + .append_header(( + "Location", + format!("/defi?success={}", urlencoding::encode(&success_message)), + )) .finish()) } - + // Helper method to get DeFi statistics fn get_defi_stats() -> serde_json::Map { let mut stats = serde_json::Map::new(); - + // Handle Option by unwrapping with expect - stats.insert("total_value_locked".to_string(), serde_json::Value::Number(serde_json::Number::from_f64(1250000.0).expect("Valid float"))); - stats.insert("providing_volume".to_string(), serde_json::Value::Number(serde_json::Number::from_f64(450000.0).expect("Valid float"))); - stats.insert("receiving_volume".to_string(), serde_json::Value::Number(serde_json::Number::from_f64(320000.0).expect("Valid float"))); - stats.insert("liquidity_pools_count".to_string(), serde_json::Value::Number(serde_json::Number::from(12))); - stats.insert("active_stakers".to_string(), serde_json::Value::Number(serde_json::Number::from(156))); - stats.insert("total_swap_volume".to_string(), serde_json::Value::Number(serde_json::Number::from_f64(780000.0).expect("Valid float"))); - + stats.insert( + "total_value_locked".to_string(), + serde_json::Value::Number( + serde_json::Number::from_f64(1250000.0).expect("Valid float"), + ), + ); + stats.insert( + "providing_volume".to_string(), + serde_json::Value::Number(serde_json::Number::from_f64(450000.0).expect("Valid float")), + ); + stats.insert( + "receiving_volume".to_string(), + serde_json::Value::Number(serde_json::Number::from_f64(320000.0).expect("Valid float")), + ); + stats.insert( + "liquidity_pools_count".to_string(), + serde_json::Value::Number(serde_json::Number::from(12)), + ); + stats.insert( + "active_stakers".to_string(), + serde_json::Value::Number(serde_json::Number::from(156)), + ); + stats.insert( + "total_swap_volume".to_string(), + serde_json::Value::Number(serde_json::Number::from_f64(780000.0).expect("Valid float")), + ); + stats } - + // Helper method to convert Asset to a JSON object for templates fn asset_to_json(asset: &Asset) -> serde_json::Map { let mut map = serde_json::Map::new(); - - map.insert("id".to_string(), serde_json::Value::String(asset.id.clone())); - map.insert("name".to_string(), serde_json::Value::String(asset.name.clone())); - map.insert("description".to_string(), serde_json::Value::String(asset.description.clone())); - map.insert("asset_type".to_string(), serde_json::Value::String(asset.asset_type.as_str().to_string())); - map.insert("status".to_string(), serde_json::Value::String(asset.status.as_str().to_string())); - + + map.insert( + "id".to_string(), + serde_json::Value::String(asset.id.clone()), + ); + map.insert( + "name".to_string(), + serde_json::Value::String(asset.name.clone()), + ); + map.insert( + "description".to_string(), + serde_json::Value::String(asset.description.clone()), + ); + map.insert( + "asset_type".to_string(), + serde_json::Value::String(asset.asset_type.as_str().to_string()), + ); + map.insert( + "status".to_string(), + serde_json::Value::String(asset.status.as_str().to_string()), + ); + // Add current valuation if let Some(latest) = asset.latest_valuation() { if let Some(num) = serde_json::Number::from_f64(latest.value) { - map.insert("current_valuation".to_string(), serde_json::Value::Number(num)); + map.insert( + "current_valuation".to_string(), + serde_json::Value::Number(num), + ); } else { - map.insert("current_valuation".to_string(), serde_json::Value::Number(serde_json::Number::from(0))); + map.insert( + "current_valuation".to_string(), + serde_json::Value::Number(serde_json::Number::from(0)), + ); } - map.insert("valuation_currency".to_string(), serde_json::Value::String(latest.currency.clone())); - map.insert("valuation_date".to_string(), serde_json::Value::String(latest.date.format("%Y-%m-%d").to_string())); + map.insert( + "valuation_currency".to_string(), + serde_json::Value::String(latest.currency.clone()), + ); + map.insert( + "valuation_date".to_string(), + serde_json::Value::String(latest.date.format("%Y-%m-%d").to_string()), + ); } else { - map.insert("current_valuation".to_string(), serde_json::Value::Number(serde_json::Number::from(0))); - map.insert("valuation_currency".to_string(), serde_json::Value::String("USD".to_string())); - map.insert("valuation_date".to_string(), serde_json::Value::String("N/A".to_string())); + map.insert( + "current_valuation".to_string(), + serde_json::Value::Number(serde_json::Number::from(0)), + ); + map.insert( + "valuation_currency".to_string(), + serde_json::Value::String("USD".to_string()), + ); + map.insert( + "valuation_date".to_string(), + serde_json::Value::String("N/A".to_string()), + ); } - + map } - + // Generate mock assets for testing fn get_mock_assets() -> Vec { // Reuse the asset controller's mock data function diff --git a/actix_mvc_app/src/controllers/flow.rs b/actix_mvc_app/src/controllers/flow.rs index 0757448..4bf4c0d 100644 --- a/actix_mvc_app/src/controllers/flow.rs +++ b/actix_mvc_app/src/controllers/flow.rs @@ -609,6 +609,7 @@ impl FlowController { /// Form for creating a new flow #[derive(Debug, Deserialize)] +#[allow(dead_code)] pub struct FlowForm { /// Flow name pub name: String, @@ -620,6 +621,7 @@ pub struct FlowForm { /// Form for marking a step as stuck #[derive(Debug, Deserialize)] +#[allow(dead_code)] pub struct StuckForm { /// Reason for being stuck pub reason: String, @@ -627,6 +629,7 @@ pub struct StuckForm { /// Form for adding a log to a step #[derive(Debug, Deserialize)] +#[allow(dead_code)] pub struct LogForm { /// Log message pub message: String, diff --git a/actix_mvc_app/src/controllers/governance.rs b/actix_mvc_app/src/controllers/governance.rs index b485c00..16cfbbd 100644 --- a/actix_mvc_app/src/controllers/governance.rs +++ b/actix_mvc_app/src/controllers/governance.rs @@ -1,24 +1,100 @@ -use actix_web::{web, HttpResponse, Responder, Result}; -use actix_session::Session; -use tera::Tera; -use serde_json::Value; -use serde::{Deserialize, Serialize}; -use chrono::{Utc, Duration}; -use crate::models::governance::{Proposal, Vote, ProposalStatus, VoteType, VotingResults}; +use crate::db::governance::{ + self, create_activity, get_all_activities, get_proposal_by_id, get_proposals, + get_recent_activities, +}; +// Note: Now using heromodels directly instead of local governance models use crate::utils::render_template; +use actix_session::Session; +use actix_web::{HttpResponse, Responder, Result, web}; +use chrono::{Duration, Utc}; +use heromodels::models::ActivityType; +use heromodels::models::governance::{Proposal, ProposalStatus}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use tera::Tera; + +use chrono::prelude::*; + +/// Simple vote type for UI display +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum VoteType { + Yes, + No, + Abstain, +} + +/// Simple vote structure for UI display +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Vote { + pub id: String, + pub proposal_id: String, + pub voter_id: i32, + pub voter_name: String, + pub vote_type: VoteType, + pub comment: Option, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +impl Vote { + pub fn new( + proposal_id: String, + voter_id: i32, + voter_name: String, + vote_type: VoteType, + comment: Option, + ) -> Self { + let now = Utc::now(); + Self { + id: uuid::Uuid::new_v4().to_string(), + proposal_id, + voter_id, + voter_name, + vote_type, + comment, + created_at: now, + updated_at: now, + } + } +} + +/// Simple voting results structure for UI display +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct VotingResults { + pub proposal_id: String, + pub yes_count: usize, + pub no_count: usize, + pub abstain_count: usize, + pub total_votes: usize, +} + +impl VotingResults { + pub fn new(proposal_id: String) -> Self { + Self { + proposal_id, + yes_count: 0, + no_count: 0, + abstain_count: 0, + total_votes: 0, + } + } +} /// Controller for handling governance-related routes pub struct GovernanceController; +#[allow(dead_code)] impl GovernanceController { /// Helper function to get user from session /// For testing purposes, this will always return a mock user fn get_user_from_session(session: &Session) -> Option { // Try to get user from session first - let session_user = session.get::("user").ok().flatten().and_then(|user_json| { - serde_json::from_str(&user_json).ok() - }); - + let session_user = session + .get::("user") + .ok() + .flatten() + .and_then(|user_json| serde_json::from_str(&user_json).ok()); + // If user is not in session, return a mock user for testing session_user.or_else(|| { // Create a mock user @@ -33,116 +109,284 @@ impl GovernanceController { }) } + /// Calculate statistics from the database + fn calculate_statistics_from_database(proposals: &[Proposal]) -> GovernanceStats { + let mut stats = GovernanceStats { + total_proposals: proposals.len(), + active_proposals: 0, + approved_proposals: 0, + rejected_proposals: 0, + draft_proposals: 0, + total_votes: 0, + participation_rate: 0.0, + }; + + // Count proposals by status + for proposal in proposals { + match proposal.status { + ProposalStatus::Active => stats.active_proposals += 1, + ProposalStatus::Approved => stats.approved_proposals += 1, + ProposalStatus::Rejected => stats.rejected_proposals += 1, + ProposalStatus::Draft => stats.draft_proposals += 1, + _ => {} // Handle other statuses if needed + } + + // Count total votes + stats.total_votes += proposal.ballots.len(); + } + + // Calculate participation rate (if there are any proposals) + if stats.total_proposals > 0 { + // This is a simplified calculation - in a real application, you would + // calculate this based on the number of eligible voters + stats.participation_rate = + (stats.total_votes as f64 / stats.total_proposals as f64) * 100.0; + } + + stats + } + /// Handles the governance dashboard page route pub async fn index(tmpl: web::Data, session: Session) -> Result { let mut ctx = tera::Context::new(); ctx.insert("active_page", "governance"); - + ctx.insert("active_tab", "dashboard"); + + // Header data + ctx.insert("page_title", "Governance Dashboard"); + ctx.insert( + "page_description", + "Participate in community decision-making", + ); + ctx.insert("show_create_button", &false); + // Add user to context (will always be available with our mock user) let user = Self::get_user_from_session(&session).unwrap(); ctx.insert("user", &user); - - // Get mock proposals for the dashboard - let mut proposals = Self::get_mock_proposals(); - + + // Get proposals from the database + let proposals = match crate::db::governance::get_proposals() { + Ok(props) => { + // println!( + // "📋 Proposals list page: Successfully loaded {} proposals from database", + // props.len() + // ); + for (i, proposal) in props.iter().enumerate() { + println!( + " Proposal {}: ID={}, title={:?}, status={:?}", + i + 1, + proposal.base_data.id, + proposal.title, + proposal.status + ); + } + props + } + Err(e) => { + println!("❌ Proposals list page: Failed to load proposals: {}", e); + ctx.insert("error", &format!("Failed to load proposals: {}", e)); + vec![] + } + }; + + // Make a copy of proposals for statistics + let proposals_for_stats = proposals.clone(); + // Filter for active proposals only - let active_proposals: Vec = proposals.into_iter() - .filter(|p| p.status == ProposalStatus::Active) + let active_proposals: Vec = proposals + .into_iter() + .filter(|p| p.status == heromodels::models::ProposalStatus::Active) .collect(); - + // Sort active proposals by voting end date (ascending) let mut sorted_active_proposals = active_proposals.clone(); - sorted_active_proposals.sort_by(|a, b| a.voting_ends_at.cmp(&b.voting_ends_at)); - + sorted_active_proposals.sort_by(|a, b| a.vote_start_date.cmp(&b.vote_end_date)); + ctx.insert("proposals", &sorted_active_proposals); - + // Get the nearest deadline proposal for the voting pane if let Some(nearest_proposal) = sorted_active_proposals.first() { + // Calculate voting results for the nearest proposal + let results = Self::calculate_voting_results_from_proposal(nearest_proposal); + + // Add both the proposal and its results to the context ctx.insert("nearest_proposal", nearest_proposal); + ctx.insert("nearest_proposal_results", &results); } - - // Get recent activity for the timeline - let recent_activity = Self::get_mock_recent_activity(); - ctx.insert("recent_activity", &recent_activity); - - // Get some statistics - let stats = Self::get_mock_statistics(); + + // Calculate statistics from the database + let stats = Self::calculate_statistics_from_database(&proposals_for_stats); ctx.insert("stats", &stats); - + + // Get recent governance activities from our tracker (limit to 4 for dashboard) + let recent_activity = match Self::get_recent_governance_activities() { + Ok(activities) => activities.into_iter().take(4).collect::>(), + Err(e) => { + eprintln!("Failed to load recent activities: {}", e); + Vec::new() + } + }; + ctx.insert("recent_activity", &recent_activity); + render_template(&tmpl, "governance/index.html", &ctx) } /// Handles the proposal list page route - pub async fn proposals(tmpl: web::Data, session: Session) -> Result { + pub async fn proposals( + query: web::Query, + tmpl: web::Data, + session: Session, + ) -> Result { let mut ctx = tera::Context::new(); ctx.insert("active_page", "governance"); ctx.insert("active_tab", "proposals"); - + + // Header data + ctx.insert("page_title", "All Proposals"); + ctx.insert( + "page_description", + "Browse and filter all governance proposals", + ); + ctx.insert("show_create_button", &false); + // Add user to context if available if let Some(user) = Self::get_user_from_session(&session) { ctx.insert("user", &user); } - - // Get mock proposals - let proposals = Self::get_mock_proposals(); + + // Get proposals from the database + let mut proposals = match get_proposals() { + Ok(props) => props, + Err(e) => { + ctx.insert("error", &format!("Failed to load proposals: {}", e)); + vec![] + } + }; + + // Filter proposals by status if provided + if let Some(status_filter) = &query.status { + if !status_filter.is_empty() { + proposals = proposals + .into_iter() + .filter(|p| { + let proposal_status = format!("{:?}", p.status); + proposal_status == *status_filter + }) + .collect(); + } + } + + // Filter by search term if provided (title or description) + if let Some(search_term) = &query.search { + if !search_term.is_empty() { + let search_term = search_term.to_lowercase(); + proposals = proposals + .into_iter() + .filter(|p| { + p.title.to_lowercase().contains(&search_term) + || p.description.to_lowercase().contains(&search_term) + }) + .collect(); + } + } + + // Add the filtered proposals to the context ctx.insert("proposals", &proposals); - + + // Add the filter values back to the context for form persistence + ctx.insert("status_filter", &query.status); + ctx.insert("search_filter", &query.search); + render_template(&tmpl, "governance/proposals.html", &ctx) } /// Handles the proposal detail page route pub async fn proposal_detail( path: web::Path, - tmpl: web::Data, - session: Session + req: actix_web::HttpRequest, + tmpl: web::Data, + session: Session, ) -> Result { + // Extract query parameters from the request + let query_str = req.query_string(); + let vote_success = query_str.contains("vote_success=true"); let proposal_id = path.into_inner(); let mut ctx = tera::Context::new(); ctx.insert("active_page", "governance"); - + ctx.insert("active_tab", "proposals"); + + // Header data + ctx.insert("page_title", "Proposal Details"); + ctx.insert( + "page_description", + "View proposal information and cast your vote", + ); + ctx.insert("show_create_button", &false); + // Add user to context if available if let Some(user) = Self::get_user_from_session(&session) { ctx.insert("user", &user); } - + // Get mock proposal detail - let proposal = Self::get_mock_proposal_by_id(&proposal_id); - if let Some(proposal) = proposal { + let proposal = get_proposal_by_id(proposal_id.parse().unwrap()); + if let Ok(Some(proposal)) = proposal { ctx.insert("proposal", &proposal); - - // Get mock votes for this proposal - let votes = Self::get_mock_votes_for_proposal(&proposal_id); + + // Extract votes directly from the proposal + let votes = Self::extract_votes_from_proposal(&proposal); ctx.insert("votes", &votes); - - // Get voting results - let results = Self::get_mock_voting_results(&proposal_id); + + // Calculate voting results directly from the proposal + let results = Self::calculate_voting_results_from_proposal(&proposal); ctx.insert("results", &results); - + + // Check if vote_success parameter is present and add success message + if vote_success { + ctx.insert("success", "Your vote has been successfully recorded!"); + } + render_template(&tmpl, "governance/proposal_detail.html", &ctx) } else { // Proposal not found ctx.insert("error", "Proposal not found"); // For the error page, we'll use a special case to set the status code to 404 match tmpl.render("error.html", &ctx) { - Ok(content) => Ok(HttpResponse::NotFound().content_type("text/html").body(content)), + Ok(content) => Ok(HttpResponse::NotFound() + .content_type("text/html") + .body(content)), Err(e) => { eprintln!("Error rendering error template: {}", e); - Err(actix_web::error::ErrorInternalServerError(format!("Error: {}", e))) + Err(actix_web::error::ErrorInternalServerError(format!( + "Error: {}", + e + ))) } } } } /// Handles the create proposal page route - pub async fn create_proposal_form(tmpl: web::Data, session: Session) -> Result { + pub async fn create_proposal_form( + tmpl: web::Data, + session: Session, + ) -> Result { let mut ctx = tera::Context::new(); ctx.insert("active_page", "governance"); ctx.insert("active_tab", "create"); - + + // Header data + ctx.insert("page_title", "Create Proposal"); + ctx.insert( + "page_description", + "Submit a new proposal for community voting", + ); + ctx.insert("show_create_button", &false); + // Add user to context (will always be available with our mock user) let user = Self::get_user_from_session(&session).unwrap(); ctx.insert("user", &user); - + render_template(&tmpl, "governance/create_proposal.html", &ctx) } @@ -150,73 +394,197 @@ impl GovernanceController { pub async fn submit_proposal( _form: web::Form, tmpl: web::Data, - session: Session + session: Session, ) -> Result { let mut ctx = tera::Context::new(); ctx.insert("active_page", "governance"); - + // Add user to context (will always be available with our mock user) let user = Self::get_user_from_session(&session).unwrap(); ctx.insert("user", &user); - - // In a real application, we would save the proposal to a database + + let proposal_title = &_form.title; + let proposal_description = &_form.description; + + // Use the DB-backed proposal creation + // Parse voting_start_date and voting_end_date from the form (YYYY-MM-DD expected) + let voting_start_date = _form.voting_start_date.as_ref().and_then(|s| { + chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d") + .ok() + .and_then(|d| d.and_hms_opt(0, 0, 0)) + .map(|naive| chrono::Utc.from_utc_datetime(&naive)) + }); + let voting_end_date = _form.voting_end_date.as_ref().and_then(|s| { + chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d") + .ok() + .and_then(|d| d.and_hms_opt(23, 59, 59)) + .map(|naive| chrono::Utc.from_utc_datetime(&naive)) + }); + + // Extract user id and name from serde_json::Value + let user_id = user + .get("id") + .and_then(|v| v.as_i64()) + .unwrap_or(1) + .to_string(); + + let user_name = user + .get("username") + .and_then(|v| v.as_str()) + .unwrap_or("Test User") + .to_string(); + + let is_draft = _form.draft.is_some(); + let status = if is_draft { + ProposalStatus::Draft + } else { + ProposalStatus::Active + }; + match governance::create_new_proposal( + &user_id, + &user_name, + proposal_title, + proposal_description, + status, + voting_start_date, + voting_end_date, + ) { + Ok((proposal_id, saved_proposal)) => { + println!( + "Proposal saved to DB: ID={}, title={:?}", + proposal_id, saved_proposal.title + ); + + // Track the proposal creation activity + let _ = create_activity( + proposal_id, + &saved_proposal.title, + &user_name, + &ActivityType::ProposalCreated, + ); + + ctx.insert("success", "Proposal created successfully!"); + } + Err(err) => { + println!("Failed to save proposal: {err}"); + ctx.insert("error", &format!("Failed to save proposal: {err}")); + } + } + // For now, we'll just redirect to the proposals page with a success message - ctx.insert("success", "Proposal created successfully!"); - - // Get mock proposals - let proposals = Self::get_mock_proposals(); + + // Get proposals from the database + let proposals = match crate::db::governance::get_proposals() { + Ok(props) => { + println!( + "✅ Successfully loaded {} proposals from database", + props.len() + ); + for (i, proposal) in props.iter().enumerate() { + println!( + " Proposal {}: ID={}, title={:?}, status={:?}", + i + 1, + proposal.base_data.id, + proposal.title, + proposal.status + ); + } + props + } + Err(e) => { + println!("❌ Failed to load proposals: {}", e); + ctx.insert("error", &format!("Failed to load proposals: {}", e)); + vec![] + } + }; ctx.insert("proposals", &proposals); - + + // Add the required context variables for the proposals template + ctx.insert("active_tab", "proposals"); + ctx.insert("status_filter", &None::); + ctx.insert("search_filter", &None::); + + // Header data (required by _header.html template) + ctx.insert("page_title", "All Proposals"); + ctx.insert( + "page_description", + "Browse and filter all governance proposals", + ); + ctx.insert("show_create_button", &false); + render_template(&tmpl, "governance/proposals.html", &ctx) } /// Handles the submission of a vote on a proposal pub async fn submit_vote( path: web::Path, - _form: web::Form, + form: web::Form, tmpl: web::Data, - session: Session + session: Session, ) -> Result { let proposal_id = path.into_inner(); - - // Check if user is logged in - if Self::get_user_from_session(&session).is_none() { - return Ok(HttpResponse::Found().append_header(("Location", "/login")).finish()); - } - let mut ctx = tera::Context::new(); ctx.insert("active_page", "governance"); - - // Add user to context if available - if let Some(user) = Self::get_user_from_session(&session) { - ctx.insert("user", &user); - } - - // Get mock proposal detail - let proposal = Self::get_mock_proposal_by_id(&proposal_id); - if let Some(proposal) = proposal { - ctx.insert("proposal", &proposal); - ctx.insert("success", "Your vote has been recorded!"); - - // Get mock votes for this proposal - let votes = Self::get_mock_votes_for_proposal(&proposal_id); - ctx.insert("votes", &votes); - - // Get voting results - let results = Self::get_mock_voting_results(&proposal_id); - ctx.insert("results", &results); - - render_template(&tmpl, "governance/proposal_detail.html", &ctx) - } else { - // Proposal not found - ctx.insert("error", "Proposal not found"); - // For the error page, we'll use a special case to set the status code to 404 - match tmpl.render("error.html", &ctx) { - Ok(content) => Ok(HttpResponse::NotFound().content_type("text/html").body(content)), - Err(e) => { - eprintln!("Error rendering error template: {}", e); - Err(actix_web::error::ErrorInternalServerError(format!("Error: {}", e))) + + // Check if user is logged in + let user = match Self::get_user_from_session(&session) { + Some(user) => user, + None => { + return Ok(HttpResponse::Found() + .append_header(("Location", "/login")) + .finish()); + } + }; + ctx.insert("user", &user); + + // Extract user ID + let user_id = user.get("id").and_then(|v| v.as_i64()).unwrap_or(1) as i32; + + // Parse proposal ID + let proposal_id_u32 = match proposal_id.parse::() { + Ok(id) => id, + Err(_) => { + ctx.insert("error", "Invalid proposal ID"); + return render_template(&tmpl, "error.html", &ctx); + } + }; + + // Submit the vote + match crate::db::governance::submit_vote_on_proposal( + proposal_id_u32, + user_id, + &form.vote_type, + 1, // Default to 1 share + form.comment.as_ref().map(|s| s.to_string()), // Pass the comment from the form + ) { + Ok(_) => { + // Record the vote activity + let user_name = user + .get("username") + .and_then(|v| v.as_str()) + .unwrap_or("Unknown User"); + + // Track the vote cast activity + if let Ok(Some(proposal)) = get_proposal_by_id(proposal_id_u32) { + let _ = create_activity( + proposal_id_u32, + &proposal.title, + user_name, + &ActivityType::VoteCast, + ); } + + // Redirect to the proposal detail page with a success message + return Ok(HttpResponse::Found() + .append_header(( + "Location", + format!("/governance/proposals/{}?vote_success=true", proposal_id), + )) + .finish()); + } + Err(e) => { + ctx.insert("error", &format!("Failed to submit vote: {}", e)); + render_template(&tmpl, "error.html", &ctx) } } } @@ -226,176 +594,155 @@ impl GovernanceController { let mut ctx = tera::Context::new(); ctx.insert("active_page", "governance"); ctx.insert("active_tab", "my_votes"); - + + // Header data + ctx.insert("page_title", "My Votes"); + ctx.insert( + "page_description", + "View your voting history and participation", + ); + ctx.insert("show_create_button", &false); + // Add user to context (will always be available with our mock user) let user = Self::get_user_from_session(&session).unwrap(); ctx.insert("user", &user); - - // Get mock votes for this user - let votes = Self::get_mock_votes_for_user(1); // Assuming user ID 1 for mock data - ctx.insert("votes", &votes); - + + // Extract user ID + let user_id = user.get("id").and_then(|v| v.as_i64()).unwrap_or(1) as i32; + + // Get all proposals from the database + let proposals = match crate::db::governance::get_proposals() { + Ok(props) => props, + Err(e) => { + ctx.insert("error", &format!("Failed to load proposals: {}", e)); + vec![] + } + }; + + // Extract votes for this user from all proposals + let mut user_votes = Vec::new(); + for proposal in &proposals { + // Extract votes from this proposal + let votes = Self::extract_votes_from_proposal(proposal); + + // Filter votes for this user + for vote in votes { + if vote.voter_id == user_id { + user_votes.push((vote, proposal.clone())); + } + } + } + + // Calculate total vote counts for all proposals + let total_vote_counts = Self::calculate_total_vote_counts(&proposals); + ctx.insert("total_yes_votes", &total_vote_counts.0); + ctx.insert("total_no_votes", &total_vote_counts.1); + ctx.insert("total_abstain_votes", &total_vote_counts.2); + + ctx.insert("votes", &user_votes); + render_template(&tmpl, "governance/my_votes.html", &ctx) } - /// Generate mock recent activity data for the dashboard - fn get_mock_recent_activity() -> Vec { - vec![ - serde_json::json!({ - "type": "vote", - "user": "Sarah Johnson", - "proposal_id": "prop-001", - "proposal_title": "Community Garden Initiative", - "action": "voted Yes", - "timestamp": (Utc::now() - Duration::hours(2)).to_rfc3339(), - "icon": "bi-check-circle-fill text-success" - }), - serde_json::json!({ - "type": "comment", - "user": "Michael Chen", - "proposal_id": "prop-003", - "proposal_title": "Weekly Community Calls", - "action": "commented", - "comment": "I think this would greatly improve communication.", - "timestamp": (Utc::now() - Duration::hours(5)).to_rfc3339(), - "icon": "bi-chat-left-text-fill text-primary" - }), - serde_json::json!({ - "type": "vote", - "user": "Robert Callingham", - "proposal_id": "prop-005", - "proposal_title": "Security Audit Implementation", - "action": "voted Yes", - "timestamp": (Utc::now() - Duration::hours(8)).to_rfc3339(), - "icon": "bi-check-circle-fill text-success" - }), - serde_json::json!({ - "type": "proposal", - "user": "Emma Rodriguez", - "proposal_id": "prop-004", - "proposal_title": "Sustainability Roadmap", - "action": "created proposal", - "timestamp": (Utc::now() - Duration::hours(12)).to_rfc3339(), - "icon": "bi-file-earmark-text-fill text-info" - }), - serde_json::json!({ - "type": "vote", - "user": "David Kim", - "proposal_id": "prop-002", - "proposal_title": "Governance Framework Update", - "action": "voted No", - "timestamp": (Utc::now() - Duration::hours(16)).to_rfc3339(), - "icon": "bi-x-circle-fill text-danger" - }), - serde_json::json!({ - "type": "comment", - "user": "Lisa Wang", - "proposal_id": "prop-001", - "proposal_title": "Community Garden Initiative", - "action": "commented", - "comment": "I'd like to volunteer to help coordinate this effort.", - "timestamp": (Utc::now() - Duration::hours(24)).to_rfc3339(), - "icon": "bi-chat-left-text-fill text-primary" - }), - ] + /// Handles the all activities page route + pub async fn all_activities(tmpl: web::Data, session: Session) -> Result { + let mut ctx = tera::Context::new(); + ctx.insert("active_page", "governance"); + ctx.insert("active_tab", "activities"); + + // Header data + ctx.insert("page_title", "All Governance Activities"); + ctx.insert( + "page_description", + "Complete history of governance actions and events", + ); + ctx.insert("show_create_button", &false); + + // Add user to context (will always be available with our mock user) + let user = Self::get_user_from_session(&session).unwrap(); + ctx.insert("user", &user); + + // Get all governance activities from the database + let activities = match Self::get_all_governance_activities() { + Ok(activities) => activities, + Err(e) => { + eprintln!("Failed to load all activities: {}", e); + Vec::new() + } + }; + ctx.insert("activities", &activities); + + render_template(&tmpl, "governance/all_activities.html", &ctx) } - // Mock data generation methods - - /// Generate mock proposals for testing - fn get_mock_proposals() -> Vec { - let now = Utc::now(); - vec![ - Proposal { - id: "prop-001".to_string(), - creator_id: 1, - creator_name: "Ibrahim Faraji".to_string(), - title: "Establish Zanzibar Digital Trade Hub".to_string(), - description: "This proposal aims to create a dedicated digital trade hub within the Zanzibar Digital Freezone to facilitate international e-commerce for local businesses. The hub will provide logistics support, digital marketing services, and regulatory compliance assistance to help Zanzibar businesses reach global markets.".to_string(), - status: ProposalStatus::Active, - created_at: now - Duration::days(5), - updated_at: now - Duration::days(5), - voting_starts_at: Some(now - Duration::days(3)), - voting_ends_at: Some(now + Duration::days(4)), - }, - Proposal { - id: "prop-002".to_string(), - creator_id: 2, - creator_name: "Amina Salim".to_string(), - title: "ZDFZ Sustainable Tourism Framework".to_string(), - description: "A comprehensive framework for sustainable tourism development within the Zanzibar Digital Freezone. This proposal outlines environmental standards, community benefit-sharing mechanisms, and digital infrastructure for eco-tourism businesses. It includes tokenization standards for tourism assets and a certification system for sustainable operators.".to_string(), - status: ProposalStatus::Approved, - created_at: now - Duration::days(15), - updated_at: now - Duration::days(2), - voting_starts_at: Some(now - Duration::days(14)), - voting_ends_at: Some(now - Duration::days(2)), - }, - Proposal { - id: "prop-003".to_string(), - creator_id: 3, - creator_name: "Hassan Mwinyi".to_string(), - title: "Spice Industry Modernization Initiative".to_string(), - description: "This proposal seeks to modernize Zanzibar's traditional spice industry through blockchain-based supply chain tracking, international quality certification, and digital marketplace integration. The initiative will help local spice farmers and processors access premium international markets while preserving traditional cultivation methods.".to_string(), - status: ProposalStatus::Draft, - created_at: now - Duration::days(1), - updated_at: now - Duration::days(1), - voting_starts_at: None, - voting_ends_at: None, - }, - Proposal { - id: "prop-004".to_string(), - creator_id: 1, - creator_name: "Ibrahim Faraji".to_string(), - title: "ZDFZ Regulatory Framework for Digital Financial Services".to_string(), - description: "Establish a comprehensive regulatory framework for digital financial services within the Zanzibar Digital Freezone. This includes licensing requirements for crypto exchanges, digital payment providers, and tokenized asset platforms operating within the zone, while ensuring compliance with international AML/KYC standards.".to_string(), - status: ProposalStatus::Rejected, - created_at: now - Duration::days(20), - updated_at: now - Duration::days(5), - voting_starts_at: Some(now - Duration::days(19)), - voting_ends_at: Some(now - Duration::days(5)), - }, - Proposal { - id: "prop-005".to_string(), - creator_id: 4, - creator_name: "Fatma Busaidy".to_string(), - title: "Digital Arts Incubator and Artwork Marketplace".to_string(), - description: "Create a dedicated digital arts incubator and Artwork marketplace to support Zanzibar's creative economy. The initiative will provide technical training, equipment, and a curated marketplace for local artists to create and sell digital art that celebrates Zanzibar's rich cultural heritage while accessing global markets.".to_string(), - status: ProposalStatus::Active, - created_at: now - Duration::days(7), - updated_at: now - Duration::days(7), - voting_starts_at: Some(now - Duration::days(6)), - voting_ends_at: Some(now + Duration::days(1)), - }, - Proposal { - id: "prop-006".to_string(), - creator_id: 5, - creator_name: "Omar Makame".to_string(), - title: "Zanzibar Renewable Energy Microgrid Network".to_string(), - description: "Develop a network of renewable energy microgrids across the Zanzibar Digital Freezone using tokenized investment and community ownership models. This proposal outlines the technical specifications, governance structure, and token economics for deploying solar and tidal energy systems that will ensure energy independence for the zone.".to_string(), - status: ProposalStatus::Active, - created_at: now - Duration::days(10), - updated_at: now - Duration::days(9), - voting_starts_at: Some(now - Duration::days(8)), - voting_ends_at: Some(now + Duration::days(6)), - }, - Proposal { - id: "prop-007".to_string(), - creator_id: 6, - creator_name: "Saida Juma".to_string(), - title: "ZDFZ Educational Technology Initiative".to_string(), - description: "Establish a comprehensive educational technology program within the Zanzibar Digital Freezone to develop local tech talent. This initiative includes coding academies, blockchain development courses, and digital entrepreneurship training, with a focus on preparing Zanzibar's youth for careers in the zone's growing digital economy.".to_string(), - status: ProposalStatus::Draft, - created_at: now - Duration::days(3), - updated_at: now - Duration::days(2), - voting_starts_at: None, - voting_ends_at: None, - }, - ] + /// Get recent governance activities from the database + fn get_recent_governance_activities() -> Result, String> { + // Get real activities from the database (no demo data) + let activities = get_recent_activities()?; + + // Convert GovernanceActivity to the format expected by the template + let formatted_activities: Vec = activities + .into_iter() + .map(|activity| { + // Map activity type to appropriate icon + let (icon, action) = match activity.activity_type.as_str() { + "proposal_created" => ("bi-plus-circle-fill text-success", "created proposal"), + "vote_cast" => ("bi-check-circle-fill text-primary", "cast vote"), + "voting_started" => ("bi-play-circle-fill text-info", "started voting"), + "voting_ended" => ("bi-clock-fill text-warning", "ended voting"), + "proposal_status_changed" => ("bi-shield-check text-success", "changed status"), + "vote_option_added" => ("bi-list-ul text-secondary", "added vote option"), + _ => ("bi-circle-fill text-muted", "performed action"), + }; + + serde_json::json!({ + "type": activity.activity_type, + "icon": icon, + "user": activity.creator_name, + "action": action, + "proposal_title": activity.proposal_title, + "created_at": activity.created_at.format("%Y-%m-%dT%H:%M:%SZ").to_string(), + "proposal_id": activity.proposal_id + }) + }) + .collect(); + + Ok(formatted_activities) } - /// Get a mock proposal by ID - fn get_mock_proposal_by_id(id: &str) -> Option { - Self::get_mock_proposals().into_iter().find(|p| p.id == id) + /// Get all governance activities from the database + fn get_all_governance_activities() -> Result, String> { + // Get all activities from the database + let activities = get_all_activities()?; + + // Convert GovernanceActivity to the format expected by the template + let formatted_activities: Vec = activities + .into_iter() + .map(|activity| { + // Map activity type to appropriate icon + let (icon, action) = match activity.activity_type.as_str() { + "proposal_created" => ("bi-plus-circle-fill text-success", "created proposal"), + "vote_cast" => ("bi-check-circle-fill text-primary", "cast vote"), + "voting_started" => ("bi-play-circle-fill text-info", "started voting"), + "voting_ended" => ("bi-clock-fill text-warning", "ended voting"), + "proposal_status_changed" => ("bi-shield-check text-success", "changed status"), + "vote_option_added" => ("bi-list-ul text-secondary", "added vote option"), + _ => ("bi-circle-fill text-muted", "performed action"), + }; + + serde_json::json!({ + "type": activity.activity_type, + "icon": icon, + "user": activity.creator_name, + "action": action, + "proposal_title": activity.proposal_title, + "created_at": activity.created_at.format("%Y-%m-%dT%H:%M:%SZ").to_string(), + "proposal_id": activity.proposal_id + }) + }) + .collect(); + + Ok(formatted_activities) } /// Generate mock votes for a specific proposal @@ -445,84 +792,151 @@ impl GovernanceController { ] } - /// Generate mock votes for a specific user - fn get_mock_votes_for_user(user_id: i32) -> Vec<(Vote, Proposal)> { - let votes = vec![ - Vote { - id: "vote-001".to_string(), - proposal_id: "prop-001".to_string(), - voter_id: user_id, - voter_name: "Robert Callingham".to_string(), - vote_type: VoteType::Yes, - comment: Some("I strongly support this initiative.".to_string()), - created_at: Utc::now() - Duration::days(2), - updated_at: Utc::now() - Duration::days(2), - }, - Vote { - id: "vote-005".to_string(), - proposal_id: "prop-002".to_string(), - voter_id: user_id, - voter_name: "Robert Callingham".to_string(), - vote_type: VoteType::No, - comment: Some("I don't think this is a priority right now.".to_string()), - created_at: Utc::now() - Duration::days(10), - updated_at: Utc::now() - Duration::days(10), - }, - Vote { - id: "vote-008".to_string(), - proposal_id: "prop-004".to_string(), - voter_id: user_id, - voter_name: "Robert Callingham".to_string(), - vote_type: VoteType::Yes, - comment: None, - created_at: Utc::now() - Duration::days(18), - updated_at: Utc::now() - Duration::days(18), - }, - Vote { - id: "vote-010".to_string(), - proposal_id: "prop-005".to_string(), - voter_id: user_id, - voter_name: "Robert Callingham".to_string(), - vote_type: VoteType::Yes, - comment: Some("Security is always a top priority.".to_string()), - created_at: Utc::now() - Duration::days(5), - updated_at: Utc::now() - Duration::days(5), - }, - ]; - - let proposals = Self::get_mock_proposals(); - votes.into_iter() - .filter_map(|vote| { - proposals.iter() - .find(|p| p.id == vote.proposal_id) - .map(|p| (vote.clone(), p.clone())) - }) - .collect() - } + /// Calculate voting results from a proposal + fn calculate_voting_results_from_proposal(proposal: &Proposal) -> VotingResults { + let mut results = VotingResults::new(proposal.base_data.id.to_string()); - /// Generate mock voting results for a proposal - fn get_mock_voting_results(proposal_id: &str) -> VotingResults { - let votes = Self::get_mock_votes_for_proposal(proposal_id); - let mut results = VotingResults::new(proposal_id.to_string()); - - for vote in votes { - results.add_vote(&vote.vote_type); + // Count votes for each option + for option in &proposal.options { + match option.id { + 1 => results.yes_count = option.count as usize, + 2 => results.no_count = option.count as usize, + 3 => results.abstain_count = option.count as usize, + _ => {} // Ignore other options + } } - + + // Calculate total votes + results.total_votes = results.yes_count + results.no_count + results.abstain_count; + results } - /// Generate mock statistics for the governance dashboard - fn get_mock_statistics() -> GovernanceStats { - GovernanceStats { - total_proposals: 5, - active_proposals: 2, - approved_proposals: 1, - rejected_proposals: 1, - draft_proposals: 1, - total_votes: 15, - participation_rate: 75.0, + /// Extract votes from a proposal's ballots + fn extract_votes_from_proposal(proposal: &Proposal) -> Vec { + let mut votes = Vec::new(); + + // Debug: Print proposal ID and number of ballots + println!( + "Extracting votes from proposal ID: {}", + proposal.base_data.id + ); + println!("Number of ballots in proposal: {}", proposal.ballots.len()); + + // If there are no ballots, create some mock votes for testing + if proposal.ballots.is_empty() { + println!("No ballots found in proposal, creating mock votes for testing"); + + // Create mock votes based on the option counts + for option in &proposal.options { + if option.count > 0 { + let vote_type = match option.id { + 1 => VoteType::Yes, + 2 => VoteType::No, + 3 => VoteType::Abstain, + _ => continue, + }; + + // Create a mock vote for each count + for i in 0..option.count { + let vote = Vote::new( + proposal.base_data.id.to_string(), + i as i32 + 1, + format!("User {}", i + 1), + vote_type.clone(), + option.comment.clone(), + ); + votes.push(vote); + } + } + } + + println!("Created {} mock votes", votes.len()); + return votes; } + + // Convert each ballot to a Vote + for (i, ballot) in proposal.ballots.iter().enumerate() { + println!( + "Processing ballot {}: user_id={}, option_id={}, shares={}", + i, ballot.user_id, ballot.vote_option_id, ballot.shares_count + ); + + // Map option_id to VoteType + let vote_type = match ballot.vote_option_id { + 1 => VoteType::Yes, + 2 => VoteType::No, + 3 => VoteType::Abstain, + _ => { + println!( + "Unknown option_id: {}, defaulting to Abstain", + ballot.vote_option_id + ); + VoteType::Abstain // Default to Abstain for unknown options + } + }; + + // Convert user_id from u32 to i32 safely + let voter_id = match i32::try_from(ballot.user_id) { + Ok(id) => id, + Err(e) => { + println!("Failed to convert user_id {} to i32: {}", ballot.user_id, e); + continue; // Skip this ballot if conversion fails + } + }; + + let ballot_timestamp = + match chrono::DateTime::from_timestamp(ballot.base_data.created_at, 0) { + Some(dt) => dt, + None => { + println!( + "Warning: Invalid timestamp {} for ballot, using current time", + ballot.base_data.created_at + ); + Utc::now() + } + }; + + let vote = Vote { + id: uuid::Uuid::new_v4().to_string(), + proposal_id: proposal.base_data.id.to_string(), + voter_id, + voter_name: format!("User {}", voter_id), + vote_type, + comment: ballot.comment.clone(), + created_at: ballot_timestamp, // This is already local time + updated_at: ballot_timestamp, // Same as created_at for votes + }; + + votes.push(vote); + } + votes + } + + // The calculate_statistics_from_database function is now defined at the top of the impl block + + /// Calculate total vote counts across all proposals + /// Returns a tuple of (yes_count, no_count, abstain_count) + fn calculate_total_vote_counts(proposals: &[Proposal]) -> (usize, usize, usize) { + let mut yes_count = 0; + let mut no_count = 0; + let mut abstain_count = 0; + + for proposal in proposals { + // Extract votes from this proposal + let votes = Self::extract_votes_from_proposal(proposal); + + // Count votes by type + for vote in votes { + match vote.vote_type { + VoteType::Yes => yes_count += 1, + VoteType::No => no_count += 1, + VoteType::Abstain => abstain_count += 1, + } + } + } + + (yes_count, no_count, abstain_count) } } @@ -533,6 +947,8 @@ pub struct ProposalForm { pub title: String, /// Description of the proposal pub description: String, + /// Status of the proposal + pub draft: Option, /// Start date for voting pub voting_start_date: Option, /// End date for voting @@ -541,6 +957,7 @@ pub struct ProposalForm { /// Represents the data submitted in the vote form #[derive(Debug, Deserialize)] +#[allow(dead_code)] pub struct VoteForm { /// Type of vote (yes, no, abstain) pub vote_type: String, @@ -548,6 +965,13 @@ pub struct VoteForm { pub comment: Option, } +/// Query parameters for filtering proposals +#[derive(Debug, Deserialize)] +pub struct ProposalQuery { + pub status: Option, + pub search: Option, +} + /// Represents statistics for the governance dashboard #[derive(Debug, Serialize)] pub struct GovernanceStats { diff --git a/actix_mvc_app/src/controllers/home.rs b/actix_mvc_app/src/controllers/home.rs index 166f2e5..bf8bf56 100644 --- a/actix_mvc_app/src/controllers/home.rs +++ b/actix_mvc_app/src/controllers/home.rs @@ -96,6 +96,7 @@ impl HomeController { /// Represents the data submitted in the contact form #[derive(Debug, serde::Deserialize)] +#[allow(dead_code)] pub struct ContactForm { pub name: String, pub email: String, diff --git a/actix_mvc_app/src/controllers/marketplace.rs b/actix_mvc_app/src/controllers/marketplace.rs index f7e3f83..72df6a2 100644 --- a/actix_mvc_app/src/controllers/marketplace.rs +++ b/actix_mvc_app/src/controllers/marketplace.rs @@ -1,12 +1,11 @@ -use actix_web::{web, HttpResponse, Result, http}; -use tera::{Context, Tera}; -use chrono::{Utc, Duration}; +use actix_web::{HttpResponse, Result, http, web}; +use chrono::{Duration, Utc}; use serde::Deserialize; -use uuid::Uuid; +use tera::{Context, Tera}; -use crate::models::asset::{Asset, AssetType, AssetStatus}; -use crate::models::marketplace::{Listing, ListingStatus, ListingType, Bid, BidStatus, MarketplaceStatistics}; use crate::controllers::asset::AssetController; +use crate::models::asset::{Asset, AssetStatus, AssetType}; +use crate::models::marketplace::{Listing, ListingStatus, ListingType, MarketplaceStatistics}; use crate::utils::render_template; #[derive(Debug, Deserialize)] @@ -22,6 +21,7 @@ pub struct ListingForm { } #[derive(Debug, Deserialize)] +#[allow(dead_code)] pub struct BidForm { pub amount: f64, pub currency: String, @@ -38,30 +38,33 @@ impl MarketplaceController { // Display the marketplace dashboard pub async fn index(tmpl: web::Data) -> Result { let mut context = Context::new(); - + let listings = Self::get_mock_listings(); let stats = MarketplaceStatistics::new(&listings); - + // Get featured listings (up to 4) - let featured_listings: Vec<&Listing> = listings.iter() + let featured_listings: Vec<&Listing> = listings + .iter() .filter(|l| l.featured && l.status == ListingStatus::Active) .take(4) .collect(); - + // Get recent listings (up to 8) - let mut recent_listings: Vec<&Listing> = listings.iter() + let mut recent_listings: Vec<&Listing> = listings + .iter() .filter(|l| l.status == ListingStatus::Active) .collect(); - + // Sort by created_at (newest first) recent_listings.sort_by(|a, b| b.created_at.cmp(&a.created_at)); let recent_listings = recent_listings.into_iter().take(8).collect::>(); - + // Get recent sales (up to 5) - let mut recent_sales: Vec<&Listing> = listings.iter() + let mut recent_sales: Vec<&Listing> = listings + .iter() .filter(|l| l.status == ListingStatus::Sold) .collect(); - + // Sort by sold_at (newest first) recent_sales.sort_by(|a, b| { let a_sold = a.sold_at.unwrap_or(a.created_at); @@ -69,88 +72,101 @@ impl MarketplaceController { b_sold.cmp(&a_sold) }); let recent_sales = recent_sales.into_iter().take(5).collect::>(); - + // Add data to context context.insert("active_page", &"marketplace"); context.insert("stats", &stats); context.insert("featured_listings", &featured_listings); context.insert("recent_listings", &recent_listings); context.insert("recent_sales", &recent_sales); - + render_template(&tmpl, "marketplace/index.html", &context) } - + // Display all marketplace listings pub async fn list_listings(tmpl: web::Data) -> Result { let mut context = Context::new(); - + let listings = Self::get_mock_listings(); - + // Filter active listings - let active_listings: Vec<&Listing> = listings.iter() + let active_listings: Vec<&Listing> = listings + .iter() .filter(|l| l.status == ListingStatus::Active) .collect(); - + context.insert("active_page", &"marketplace"); context.insert("listings", &active_listings); - context.insert("listing_types", &[ - ListingType::FixedPrice.as_str(), - ListingType::Auction.as_str(), - ListingType::Exchange.as_str(), - ]); - context.insert("asset_types", &[ - AssetType::Token.as_str(), - AssetType::Artwork.as_str(), - AssetType::RealEstate.as_str(), - AssetType::IntellectualProperty.as_str(), - AssetType::Commodity.as_str(), - AssetType::Share.as_str(), - AssetType::Bond.as_str(), - AssetType::Other.as_str(), - ]); - + context.insert( + "listing_types", + &[ + ListingType::FixedPrice.as_str(), + ListingType::Auction.as_str(), + ListingType::Exchange.as_str(), + ], + ); + context.insert( + "asset_types", + &[ + AssetType::Token.as_str(), + AssetType::Artwork.as_str(), + AssetType::RealEstate.as_str(), + AssetType::IntellectualProperty.as_str(), + AssetType::Commodity.as_str(), + AssetType::Share.as_str(), + AssetType::Bond.as_str(), + AssetType::Other.as_str(), + ], + ); + render_template(&tmpl, "marketplace/listings.html", &context) } - + // Display my listings pub async fn my_listings(tmpl: web::Data) -> Result { let mut context = Context::new(); - + let listings = Self::get_mock_listings(); - + // Filter by current user (mock user ID) let user_id = "user-123"; - let my_listings: Vec<&Listing> = listings.iter() - .filter(|l| l.seller_id == user_id) - .collect(); - + let my_listings: Vec<&Listing> = + listings.iter().filter(|l| l.seller_id == user_id).collect(); + context.insert("active_page", &"marketplace"); context.insert("listings", &my_listings); - + render_template(&tmpl, "marketplace/my_listings.html", &context) } - + // Display listing details - pub async fn listing_detail(tmpl: web::Data, path: web::Path) -> Result { + pub async fn listing_detail( + tmpl: web::Data, + path: web::Path, + ) -> Result { let listing_id = path.into_inner(); let mut context = Context::new(); - + let listings = Self::get_mock_listings(); - + // Find the listing let listing = listings.iter().find(|l| l.id == listing_id); - + if let Some(listing) = listing { // Get similar listings (same asset type, active) - let similar_listings: Vec<&Listing> = listings.iter() - .filter(|l| l.asset_type == listing.asset_type && - l.status == ListingStatus::Active && - l.id != listing.id) + let similar_listings: Vec<&Listing> = listings + .iter() + .filter(|l| { + l.asset_type == listing.asset_type + && l.status == ListingStatus::Active + && l.id != listing.id + }) .take(4) .collect(); - + // Get highest bid amount and minimum bid for auction listings - let (highest_bid_amount, minimum_bid) = if listing.listing_type == ListingType::Auction { + let (highest_bid_amount, minimum_bid) = if listing.listing_type == ListingType::Auction + { if let Some(bid) = listing.highest_bid() { (Some(bid.amount), bid.amount + 1.0) } else { @@ -159,74 +175,79 @@ impl MarketplaceController { } else { (None, 0.0) }; - + context.insert("active_page", &"marketplace"); context.insert("listing", listing); context.insert("similar_listings", &similar_listings); context.insert("highest_bid_amount", &highest_bid_amount); context.insert("minimum_bid", &minimum_bid); - + // Add current user info for bid/purchase forms let user_id = "user-123"; let user_name = "Alice Hostly"; context.insert("user_id", &user_id); context.insert("user_name", &user_name); - + render_template(&tmpl, "marketplace/listing_detail.html", &context) } else { Ok(HttpResponse::NotFound().finish()) } } - + // Display create listing form pub async fn create_listing_form(tmpl: web::Data) -> Result { let mut context = Context::new(); - + // Get user's assets for selection let assets = AssetController::get_mock_assets(); let user_id = "user-123"; // Mock user ID - - let user_assets: Vec<&Asset> = assets.iter() + + let user_assets: Vec<&Asset> = assets + .iter() .filter(|a| a.owner_id == user_id && a.status == AssetStatus::Active) .collect(); - + context.insert("active_page", &"marketplace"); context.insert("assets", &user_assets); - context.insert("listing_types", &[ - ListingType::FixedPrice.as_str(), - ListingType::Auction.as_str(), - ListingType::Exchange.as_str(), - ]); - + context.insert( + "listing_types", + &[ + ListingType::FixedPrice.as_str(), + ListingType::Auction.as_str(), + ListingType::Exchange.as_str(), + ], + ); + render_template(&tmpl, "marketplace/create_listing.html", &context) } - + // Create a new listing pub async fn create_listing( tmpl: web::Data, form: web::Form, ) -> Result { let form = form.into_inner(); - + // Get the asset details let assets = AssetController::get_mock_assets(); let asset = assets.iter().find(|a| a.id == form.asset_id); - + if let Some(asset) = asset { // Process tags let tags = match form.tags { - Some(tags_str) => tags_str.split(',') + Some(tags_str) => tags_str + .split(',') .map(|s| s.trim().to_string()) .filter(|s| !s.is_empty()) .collect(), None => Vec::new(), }; - + // Calculate expiration date if provided - let expires_at = form.duration_days.map(|days| { - Utc::now() + Duration::days(days as i64) - }); - + let expires_at = form + .duration_days + .map(|days| Utc::now() + Duration::days(days as i64)); + // Parse listing type let listing_type = match form.listing_type.as_str() { "Fixed Price" => ListingType::FixedPrice, @@ -234,11 +255,11 @@ impl MarketplaceController { "Exchange" => ListingType::Exchange, _ => ListingType::FixedPrice, }; - + // Mock user data let user_id = "user-123"; let user_name = "Alice Hostly"; - + // Create the listing let _listing = Listing::new( form.title, @@ -255,9 +276,9 @@ impl MarketplaceController { tags, asset.image_url.clone(), ); - + // In a real application, we would save the listing to a database here - + // Redirect to the marketplace Ok(HttpResponse::SeeOther() .insert_header((http::header::LOCATION, "/marketplace")) @@ -267,94 +288,101 @@ impl MarketplaceController { let mut context = Context::new(); context.insert("active_page", &"marketplace"); context.insert("error", &"Asset not found"); - + render_template(&tmpl, "marketplace/create_listing.html", &context) } } - + // Submit a bid on an auction listing + #[allow(dead_code)] pub async fn submit_bid( - tmpl: web::Data, + _tmpl: web::Data, path: web::Path, - form: web::Form, + _form: web::Form, ) -> Result { let listing_id = path.into_inner(); - let form = form.into_inner(); - + let _form = _form.into_inner(); + // In a real application, we would: // 1. Find the listing in the database // 2. Validate the bid // 3. Create the bid // 4. Save it to the database - + // For now, we'll just redirect back to the listing Ok(HttpResponse::SeeOther() - .insert_header((http::header::LOCATION, format!("/marketplace/{}", listing_id))) + .insert_header(( + http::header::LOCATION, + format!("/marketplace/{}", listing_id), + )) .finish()) } - + // Purchase a fixed-price listing pub async fn purchase_listing( - tmpl: web::Data, + _tmpl: web::Data, path: web::Path, form: web::Form, ) -> Result { let listing_id = path.into_inner(); let form = form.into_inner(); - + if !form.agree_to_terms { // User must agree to terms return Ok(HttpResponse::SeeOther() - .insert_header((http::header::LOCATION, format!("/marketplace/{}", listing_id))) + .insert_header(( + http::header::LOCATION, + format!("/marketplace/{}", listing_id), + )) .finish()); } - + // In a real application, we would: // 1. Find the listing in the database // 2. Validate the purchase // 3. Process the transaction // 4. Update the listing status // 5. Transfer the asset - + // For now, we'll just redirect to the marketplace Ok(HttpResponse::SeeOther() .insert_header((http::header::LOCATION, "/marketplace")) .finish()) } - + // Cancel a listing pub async fn cancel_listing( - tmpl: web::Data, + _tmpl: web::Data, path: web::Path, ) -> Result { let _listing_id = path.into_inner(); - + // In a real application, we would: // 1. Find the listing in the database // 2. Validate that the current user is the seller // 3. Update the listing status - + // For now, we'll just redirect to my listings Ok(HttpResponse::SeeOther() .insert_header((http::header::LOCATION, "/marketplace/my")) .finish()) } - + // Generate mock listings for development pub fn get_mock_listings() -> Vec { let assets = AssetController::get_mock_assets(); let mut listings = Vec::new(); - + // Mock user data let user_ids = vec!["user-123", "user-456", "user-789"]; let user_names = vec!["Alice Hostly", "Ethan Cloudman", "Priya Servera"]; - + // Create some fixed price listings for i in 0..6 { let asset_index = i % assets.len(); let asset = &assets[asset_index]; let user_index = i % user_ids.len(); - + let price = match asset.asset_type { AssetType::Token => 50.0 + (i as f64 * 10.0), AssetType::Artwork => 500.0 + (i as f64 * 100.0), @@ -365,10 +393,13 @@ impl MarketplaceController { AssetType::Bond => 1500.0 + (i as f64 * 300.0), AssetType::Other => 800.0 + (i as f64 * 150.0), }; - + let mut listing = Listing::new( format!("{} for Sale", asset.name), - format!("This is a great opportunity to own {}. {}", asset.name, asset.description), + format!( + "This is a great opportunity to own {}. {}", + asset.name, asset.description + ), asset.id.clone(), asset.name.clone(), asset.asset_type.clone(), @@ -381,21 +412,21 @@ impl MarketplaceController { vec!["digital".to_string(), "asset".to_string()], asset.image_url.clone(), ); - + // Make some listings featured if i % 5 == 0 { listing.set_featured(true); } - + listings.push(listing); } - + // Create some auction listings for i in 0..4 { let asset_index = (i + 6) % assets.len(); let asset = &assets[asset_index]; let user_index = i % user_ids.len(); - + let starting_price = match asset.asset_type { AssetType::Token => 40.0 + (i as f64 * 5.0), AssetType::Artwork => 400.0 + (i as f64 * 50.0), @@ -406,7 +437,7 @@ impl MarketplaceController { AssetType::Bond => 1200.0 + (i as f64 * 250.0), AssetType::Other => 600.0 + (i as f64 * 120.0), }; - + let mut listing = Listing::new( format!("Auction: {}", asset.name), format!("Bid on this amazing {}. {}", asset.name, asset.description), @@ -422,12 +453,13 @@ impl MarketplaceController { vec!["auction".to_string(), "bidding".to_string()], asset.image_url.clone(), ); - + // Add some bids to the auctions let num_bids = 2 + (i % 3); for j in 0..num_bids { let bidder_index = (j + 1) % user_ids.len(); - if bidder_index != user_index { // Ensure seller isn't bidding + if bidder_index != user_index { + // Ensure seller isn't bidding let bid_amount = starting_price * (1.0 + (0.1 * (j + 1) as f64)); let _ = listing.add_bid( user_ids[bidder_index].to_string(), @@ -437,21 +469,21 @@ impl MarketplaceController { ); } } - + // Make some listings featured if i % 3 == 0 { listing.set_featured(true); } - + listings.push(listing); } - + // Create some exchange listings for i in 0..3 { let asset_index = (i + 10) % assets.len(); let asset = &assets[asset_index]; let user_index = i % user_ids.len(); - + let value = match asset.asset_type { AssetType::Token => 60.0 + (i as f64 * 15.0), AssetType::Artwork => 600.0 + (i as f64 * 150.0), @@ -462,33 +494,36 @@ impl MarketplaceController { AssetType::Bond => 1800.0 + (i as f64 * 350.0), AssetType::Other => 1000.0 + (i as f64 * 200.0), }; - + let listing = Listing::new( format!("Trade: {}", asset.name), - format!("Looking to exchange {} for another asset of similar value. Interested in NFTs and tokens.", asset.name), + format!( + "Looking to exchange {} for another asset of similar value. Interested in NFTs and tokens.", + asset.name + ), asset.id.clone(), asset.name.clone(), asset.asset_type.clone(), user_ids[user_index].to_string(), user_names[user_index].to_string(), - value, // Estimated value for exchange + value, // Estimated value for exchange "USD".to_string(), ListingType::Exchange, Some(Utc::now() + Duration::days(60)), vec!["exchange".to_string(), "trade".to_string()], asset.image_url.clone(), ); - + listings.push(listing); } - + // Create some sold listings for i in 0..5 { let asset_index = (i + 13) % assets.len(); let asset = &assets[asset_index]; let seller_index = i % user_ids.len(); let buyer_index = (i + 1) % user_ids.len(); - + let price = match asset.asset_type { AssetType::Token => 55.0 + (i as f64 * 12.0), AssetType::Artwork => 550.0 + (i as f64 * 120.0), @@ -499,9 +534,9 @@ impl MarketplaceController { AssetType::Bond => 1650.0 + (i as f64 * 330.0), AssetType::Other => 900.0 + (i as f64 * 180.0), }; - - let sale_price = price * 0.95; // Slight discount on sale - + + let sale_price = price * 0.95; // Slight discount on sale + let mut listing = Listing::new( format!("{} - SOLD", asset.name), format!("This {} was sold recently.", asset.name), @@ -517,27 +552,27 @@ impl MarketplaceController { vec!["sold".to_string()], asset.image_url.clone(), ); - + // Mark as sold let _ = listing.mark_as_sold( user_ids[buyer_index].to_string(), user_names[buyer_index].to_string(), sale_price, ); - + // Set sold date to be sometime in the past let days_ago = i as i64 + 1; listing.sold_at = Some(Utc::now() - Duration::days(days_ago)); - + listings.push(listing); } - + // Create a few cancelled listings for i in 0..2 { let asset_index = (i + 18) % assets.len(); let asset = &assets[asset_index]; let user_index = i % user_ids.len(); - + let price = match asset.asset_type { AssetType::Token => 45.0 + (i as f64 * 8.0), AssetType::Artwork => 450.0 + (i as f64 * 80.0), @@ -548,7 +583,7 @@ impl MarketplaceController { AssetType::Bond => 1350.0 + (i as f64 * 270.0), AssetType::Other => 750.0 + (i as f64 * 150.0), }; - + let mut listing = Listing::new( format!("{} - Cancelled", asset.name), format!("This listing for {} was cancelled.", asset.name), @@ -564,13 +599,13 @@ impl MarketplaceController { vec!["cancelled".to_string()], asset.image_url.clone(), ); - + // Cancel the listing let _ = listing.cancel(); - + listings.push(listing); } - + listings } } diff --git a/actix_mvc_app/src/db/calendar.rs b/actix_mvc_app/src/db/calendar.rs new file mode 100644 index 0000000..5af4c8d --- /dev/null +++ b/actix_mvc_app/src/db/calendar.rs @@ -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, + 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::() + .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, + end_time: DateTime, + location: Option<&str>, + color: Option<&str>, + all_day: bool, + created_by: Option, + category: Option<&str>, + reminder_minutes: Option, +) -> 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::().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. +pub fn get_calendars() -> Result, String> { + let db = get_db().map_err(|e| format!("DB error: {}", e))?; + let collection = db + .collection::() + .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. +pub fn get_events() -> Result, String> { + let db = get_db().map_err(|e| format!("DB error: {}", e))?; + let collection = db.collection::().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, String> { + let db = get_db().map_err(|e| format!("DB error: {}", e))?; + let collection = db + .collection::() + .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, String> { + let db = get_db().map_err(|e| format!("DB error: {}", e))?; + let collection = db + .collection::() + .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::() + .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, String> { + let db = get_db().map_err(|e| format!("DB error: {}", e))?; + let collection = db + .collection::() + .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 { + let db = get_db().map_err(|e| format!("DB error: {}", e))?; + let collection = db + .collection::() + .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 { + let db = get_db().map_err(|e| format!("DB error: {}", e))?; + let collection = db + .collection::() + .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 { + let db = get_db().map_err(|e| format!("DB error: {}", e))?; + let collection = db + .collection::() + .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 { + let db = get_db().map_err(|e| format!("DB error: {}", e))?; + let collection = db + .collection::() + .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 { + let db = get_db().map_err(|e| format!("DB error: {}", e))?; + let collection = db + .collection::() + .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::() + .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::() + .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 { + let db = get_db().map_err(|e| format!("DB error: {}", e))?; + let collection = db + .collection::() + .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) +} diff --git a/actix_mvc_app/src/db/db.rs b/actix_mvc_app/src/db/db.rs new file mode 100644 index 0000000..6427c14 --- /dev/null +++ b/actix_mvc_app/src/db/db.rs @@ -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 { + 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) +} diff --git a/actix_mvc_app/src/db/governance.rs b/actix_mvc_app/src/db/governance.rs new file mode 100644 index 0000000..2f1f51b --- /dev/null +++ b/actix_mvc_app/src/db/governance.rs @@ -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>, + voting_end_date: Option>, +) -> 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::() + .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. +pub fn get_proposals() -> Result, String> { + let db = get_db().map_err(|e| format!("DB error: {}", e))?; + let collection = db + .collection::() + .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, String> { + let db = get_db().map_err(|e| format!("DB error: {}", e))?; + let collection = db + .collection::() + .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, +) -> Result { + // Get the proposal from the database + let db = get_db().map_err(|e| format!("DB error: {}", e))?; + let collection = db + .collection::() + .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::() + .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, String> { + let db = get_db().expect("Can get DB"); + let collection = db + .collection::() + .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, String> { + let db = get_db().expect("Can get DB"); + let collection = db + .collection::() + .map_err(|e| format!("Collection error: {:?}", e))?; + + let db_activities = collection + .get_all() + .map_err(|e| format!("DB fetch error: {:?}", e))?; + + Ok(db_activities) +} diff --git a/actix_mvc_app/src/db/mod.rs b/actix_mvc_app/src/db/mod.rs new file mode 100644 index 0000000..951511e --- /dev/null +++ b/actix_mvc_app/src/db/mod.rs @@ -0,0 +1,4 @@ +pub mod calendar; +pub mod contracts; +pub mod db; +pub mod governance; diff --git a/actix_mvc_app/src/main.rs b/actix_mvc_app/src/main.rs index b129b76..76442e6 100644 --- a/actix_mvc_app/src/main.rs +++ b/actix_mvc_app/src/main.rs @@ -1,22 +1,23 @@ use actix_files as fs; -use actix_web::{App, HttpServer, web}; use actix_web::middleware::Logger; -use tera::Tera; -use std::io; -use std::env; +use actix_web::{App, HttpServer, web}; use lazy_static::lazy_static; +use std::env; +use std::io; +use tera::Tera; mod config; mod controllers; +mod db; mod middleware; mod models; mod routes; mod utils; // Import middleware components -use middleware::{RequestTimer, SecurityHeaders, JwtAuth}; -use utils::redis_service; +use middleware::{JwtAuth, RequestTimer, SecurityHeaders}; use models::initialize_mock_data; +use utils::redis_service; // Initialize lazy_static for in-memory storage extern crate lazy_static; @@ -29,13 +30,13 @@ lazy_static! { // Create a key that's at least 64 bytes long "my_secret_session_key_that_is_at_least_64_bytes_long_for_security_reasons_1234567890abcdef".to_string() }); - + // Ensure the key is at least 64 bytes let mut key_bytes = secret.as_bytes().to_vec(); while key_bytes.len() < 64 { key_bytes.extend_from_slice(b"0123456789abcdef"); } - + actix_web::cookie::Key::from(&key_bytes[0..64]) }; } @@ -45,14 +46,14 @@ async fn main() -> io::Result<()> { // Initialize environment dotenv::dotenv().ok(); env_logger::init_from_env(env_logger::Env::default().default_filter_or("info")); - + // Load configuration let config = config::get_config(); - + // Check for port override from command line arguments let args: Vec = env::args().collect(); let mut port = config.server.port; - + for i in 1..args.len() { if args[i] == "--port" && i + 1 < args.len() { if let Ok(p) = args[i + 1].parse::() { @@ -61,24 +62,28 @@ async fn main() -> io::Result<()> { } } } - + let bind_address = format!("{}:{}", config.server.host, port); - + // Initialize Redis client - let redis_url = std::env::var("REDIS_URL").unwrap_or_else(|_| "redis://127.0.0.1:6379".to_string()); + let redis_url = + std::env::var("REDIS_URL").unwrap_or_else(|_| "redis://127.0.0.1:6379".to_string()); if let Err(e) = redis_service::init_redis_client(&redis_url) { log::error!("Failed to initialize Redis client: {}", e); log::warn!("Calendar functionality will not work properly without Redis"); } else { log::info!("Redis client initialized successfully"); } - + // Initialize mock data for DeFi operations initialize_mock_data(); log::info!("DeFi mock data initialized successfully"); - + + // Governance activity tracker is now ready to record real user activities + log::info!("Governance activity tracker initialized and ready"); + log::info!("Starting server at http://{}", bind_address); - + // Create and configure the HTTP server HttpServer::new(move || { // Initialize Tera templates @@ -89,10 +94,10 @@ async fn main() -> io::Result<()> { ::std::process::exit(1); } }; - + // Register custom Tera functions utils::register_tera_functions(&mut tera); - + App::new() // Enable logger middleware .wrap(Logger::default()) diff --git a/actix_mvc_app/src/models/asset.rs b/actix_mvc_app/src/models/asset.rs index f2ed183..0b94b22 100644 --- a/actix_mvc_app/src/models/asset.rs +++ b/actix_mvc_app/src/models/asset.rs @@ -112,6 +112,7 @@ pub struct Asset { pub external_url: Option, } +#[allow(dead_code)] impl Asset { /// Creates a new asset pub fn new( diff --git a/actix_mvc_app/src/models/calendar.rs b/actix_mvc_app/src/models/calendar.rs index d62a77e..d90f1f3 100644 --- a/actix_mvc_app/src/models/calendar.rs +++ b/actix_mvc_app/src/models/calendar.rs @@ -1,61 +1,4 @@ -use chrono::{DateTime, Utc}; -use serde::{Deserialize, Serialize}; -use uuid::Uuid; - -/// Represents a calendar event -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CalendarEvent { - /// Unique identifier for the event - pub id: String, - /// Title of the event - pub title: String, - /// Description of the event - pub description: String, - /// Start time of the event - pub start_time: DateTime, - /// End time of the event - pub end_time: DateTime, - /// Color of the event (hex code) - pub color: String, - /// Whether the event is an all-day event - pub all_day: bool, - /// User ID of the event creator - pub user_id: Option, -} - -impl CalendarEvent { - /// Creates a new calendar event - pub fn new( - title: String, - description: String, - start_time: DateTime, - end_time: DateTime, - color: Option, - all_day: bool, - user_id: Option, - ) -> Self { - Self { - id: Uuid::new_v4().to_string(), - title, - description, - start_time, - end_time, - color: color.unwrap_or_else(|| "#4285F4".to_string()), // Google Calendar blue - all_day, - user_id, - } - } - - /// Converts the event to a JSON string - pub fn to_json(&self) -> Result { - serde_json::to_string(self) - } - - /// Creates an event from a JSON string - pub fn from_json(json: &str) -> Result { - serde_json::from_str(json) - } -} +// No imports needed for this module currently /// Represents a view mode for the calendar #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -91,4 +34,4 @@ impl CalendarViewMode { Self::Day => "day", } } -} \ No newline at end of file +} diff --git a/actix_mvc_app/src/models/contract.rs b/actix_mvc_app/src/models/contract.rs index ef936a5..e3f3035 100644 --- a/actix_mvc_app/src/models/contract.rs +++ b/actix_mvc_app/src/models/contract.rs @@ -85,6 +85,7 @@ pub struct ContractSigner { pub comments: Option, } +#[allow(dead_code)] impl ContractSigner { /// Creates a new contract signer pub fn new(name: String, email: String) -> Self { @@ -123,6 +124,7 @@ pub struct ContractRevision { pub comments: Option, } +#[allow(dead_code)] impl ContractRevision { /// Creates a new contract revision pub fn new(version: u32, content: String, created_by: String, comments: Option) -> Self { @@ -166,6 +168,7 @@ pub struct Contract { pub toc: Option>, } +#[allow(dead_code)] impl Contract { /// Creates a new contract pub fn new(title: String, description: String, contract_type: ContractType, created_by: String, organization_id: Option) -> Self { diff --git a/actix_mvc_app/src/models/defi.rs b/actix_mvc_app/src/models/defi.rs index d1986b0..8b9b5a3 100644 --- a/actix_mvc_app/src/models/defi.rs +++ b/actix_mvc_app/src/models/defi.rs @@ -14,6 +14,7 @@ pub enum DefiPositionStatus { Cancelled } +#[allow(dead_code)] impl DefiPositionStatus { pub fn as_str(&self) -> &str { match self { @@ -35,6 +36,7 @@ pub enum DefiPositionType { Collateral, } +#[allow(dead_code)] impl DefiPositionType { pub fn as_str(&self) -> &str { match self { @@ -95,6 +97,7 @@ pub struct DefiDatabase { receiving_positions: HashMap, } +#[allow(dead_code)] impl DefiDatabase { pub fn new() -> Self { Self { diff --git a/actix_mvc_app/src/models/flow.rs b/actix_mvc_app/src/models/flow.rs index 6feab01..2293c4a 100644 --- a/actix_mvc_app/src/models/flow.rs +++ b/actix_mvc_app/src/models/flow.rs @@ -110,6 +110,7 @@ pub struct FlowStep { pub logs: Vec, } +#[allow(dead_code)] impl FlowStep { /// Creates a new flow step pub fn new(name: String, description: String, order: u32) -> Self { @@ -189,6 +190,7 @@ pub struct FlowLog { pub timestamp: DateTime, } +#[allow(dead_code)] impl FlowLog { /// Creates a new flow log pub fn new(message: String) -> Self { @@ -231,6 +233,7 @@ pub struct Flow { pub current_step: Option, } +#[allow(dead_code)] impl Flow { /// Creates a new flow pub fn new(name: &str, description: &str, flow_type: FlowType, owner_id: &str, owner_name: &str) -> Self { diff --git a/actix_mvc_app/src/models/governance.rs b/actix_mvc_app/src/models/governance.rs deleted file mode 100644 index 1c4f1f3..0000000 --- a/actix_mvc_app/src/models/governance.rs +++ /dev/null @@ -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, - /// Date and time when the proposal was last updated - pub updated_at: DateTime, - /// Date and time when voting starts - pub voting_starts_at: Option>, - /// Date and time when voting ends - pub voting_ends_at: Option>, -} - -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, ends_at: DateTime) { - 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, - /// Date and time when the vote was cast - pub created_at: DateTime, - /// Date and time when the vote was last updated - pub updated_at: DateTime, -} - -impl Vote { - /// Creates a new vote - pub fn new(proposal_id: String, voter_id: i32, voter_name: String, vote_type: VoteType, comment: Option) -> 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) { - 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, - /// Filter by creator ID - pub creator_id: Option, - /// Search term for title and description - pub search: Option, -} - -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 - } -} diff --git a/actix_mvc_app/src/models/marketplace.rs b/actix_mvc_app/src/models/marketplace.rs index 784a53b..d502140 100644 --- a/actix_mvc_app/src/models/marketplace.rs +++ b/actix_mvc_app/src/models/marketplace.rs @@ -1,7 +1,7 @@ +use crate::models::asset::AssetType; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; -use crate::models::asset::{Asset, AssetType}; /// Status of a marketplace listing #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] @@ -12,6 +12,7 @@ pub enum ListingStatus { Expired, } +#[allow(dead_code)] impl ListingStatus { pub fn as_str(&self) -> &str { match self { @@ -63,6 +64,7 @@ pub enum BidStatus { Cancelled, } +#[allow(dead_code)] impl BidStatus { pub fn as_str(&self) -> &str { match self { @@ -103,6 +105,7 @@ pub struct Listing { pub image_url: Option, } +#[allow(dead_code)] impl Listing { /// Creates a new listing pub fn new( @@ -150,7 +153,13 @@ impl Listing { } /// Adds a bid to the listing - pub fn add_bid(&mut self, bidder_id: String, bidder_name: String, amount: f64, currency: String) -> Result<(), String> { + pub fn add_bid( + &mut self, + bidder_id: String, + bidder_name: String, + amount: f64, + currency: String, + ) -> Result<(), String> { if self.status != ListingStatus::Active { return Err("Listing is not active".to_string()); } @@ -160,7 +169,10 @@ impl Listing { } if currency != self.currency { - return Err(format!("Currency mismatch: expected {}, got {}", self.currency, currency)); + return Err(format!( + "Currency mismatch: expected {}, got {}", + self.currency, currency + )); } // Check if bid amount is higher than current highest bid or starting price @@ -193,13 +205,19 @@ impl Listing { /// Gets the highest bid on the listing pub fn highest_bid(&self) -> Option<&Bid> { - self.bids.iter() + self.bids + .iter() .filter(|b| b.status == BidStatus::Active) .max_by(|a, b| a.amount.partial_cmp(&b.amount).unwrap()) } /// Marks the listing as sold - pub fn mark_as_sold(&mut self, buyer_id: String, buyer_name: String, sale_price: f64) -> Result<(), String> { + pub fn mark_as_sold( + &mut self, + buyer_id: String, + buyer_name: String, + sale_price: f64, + ) -> Result<(), String> { if self.status != ListingStatus::Active { return Err("Listing is not active".to_string()); } @@ -257,11 +275,13 @@ impl MarketplaceStatistics { let mut listings_by_type = std::collections::HashMap::new(); let mut sales_by_asset_type = std::collections::HashMap::new(); - let active_listings = listings.iter() + let active_listings = listings + .iter() .filter(|l| l.status == ListingStatus::Active) .count(); - let sold_listings = listings.iter() + let sold_listings = listings + .iter() .filter(|l| l.status == ListingStatus::Sold) .count(); diff --git a/actix_mvc_app/src/models/mod.rs b/actix_mvc_app/src/models/mod.rs index 2e9448b..de69970 100644 --- a/actix_mvc_app/src/models/mod.rs +++ b/actix_mvc_app/src/models/mod.rs @@ -1,17 +1,16 @@ // Export models -pub mod user; -pub mod ticket; -pub mod calendar; -pub mod governance; -pub mod flow; -pub mod contract; pub mod asset; -pub mod marketplace; +pub mod calendar; +pub mod contract; pub mod defi; +pub mod flow; + +pub mod marketplace; +pub mod ticket; +pub mod user; // Re-export models for easier imports +pub use calendar::CalendarViewMode; +pub use defi::initialize_mock_data; +pub use ticket::{Ticket, TicketComment, TicketPriority, TicketStatus}; pub use user::User; -pub use ticket::{Ticket, TicketComment, TicketStatus, TicketPriority}; -pub use calendar::{CalendarEvent, CalendarViewMode}; -pub use marketplace::{Listing, ListingStatus, ListingType, Bid, BidStatus, MarketplaceStatistics}; -pub use defi::{DefiPosition, DefiPositionType, DefiPositionStatus, ProvidingPosition, ReceivingPosition, DEFI_DB, initialize_mock_data}; diff --git a/actix_mvc_app/src/models/ticket.rs b/actix_mvc_app/src/models/ticket.rs index 5e02d91..e8caeed 100644 --- a/actix_mvc_app/src/models/ticket.rs +++ b/actix_mvc_app/src/models/ticket.rs @@ -76,6 +76,7 @@ pub struct Ticket { pub assigned_to: Option, } +#[allow(dead_code)] impl Ticket { /// Creates a new ticket pub fn new(user_id: i32, title: String, description: String, priority: TicketPriority) -> Self { diff --git a/actix_mvc_app/src/models/user.rs b/actix_mvc_app/src/models/user.rs index aec201f..a2af576 100644 --- a/actix_mvc_app/src/models/user.rs +++ b/actix_mvc_app/src/models/user.rs @@ -4,6 +4,7 @@ use bcrypt::{hash, verify, DEFAULT_COST}; /// Represents a user in the system #[derive(Debug, Clone, Serialize, Deserialize)] +#[allow(dead_code)] pub struct User { /// Unique identifier for the user pub id: Option, @@ -31,6 +32,7 @@ pub enum UserRole { Admin, } +#[allow(dead_code)] impl User { /// Creates a new user with default values pub fn new(name: String, email: String) -> Self { @@ -125,6 +127,7 @@ impl User { /// Represents user login credentials #[derive(Debug, Deserialize)] +#[allow(dead_code)] pub struct LoginCredentials { pub email: String, pub password: String, @@ -132,6 +135,7 @@ pub struct LoginCredentials { /// Represents user registration data #[derive(Debug, Deserialize)] +#[allow(dead_code)] pub struct RegistrationData { pub name: String, pub email: String, diff --git a/actix_mvc_app/src/routes/mod.rs b/actix_mvc_app/src/routes/mod.rs index b9810f6..d1224a9 100644 --- a/actix_mvc_app/src/routes/mod.rs +++ b/actix_mvc_app/src/routes/mod.rs @@ -1,28 +1,26 @@ -use actix_web::web; -use actix_session::{SessionMiddleware, storage::CookieSessionStore}; -use crate::controllers::home::HomeController; -use crate::controllers::auth::AuthController; -use crate::controllers::ticket::TicketController; -use crate::controllers::calendar::CalendarController; -use crate::controllers::governance::GovernanceController; -use crate::controllers::flow::FlowController; -use crate::controllers::contract::ContractController; -use crate::controllers::asset::AssetController; -use crate::controllers::marketplace::MarketplaceController; -use crate::controllers::defi::DefiController; -use crate::controllers::company::CompanyController; -use crate::middleware::JwtAuth; use crate::SESSION_KEY; +use crate::controllers::asset::AssetController; +use crate::controllers::auth::AuthController; +use crate::controllers::calendar::CalendarController; +use crate::controllers::company::CompanyController; +use crate::controllers::contract::ContractController; +use crate::controllers::defi::DefiController; +use crate::controllers::flow::FlowController; +use crate::controllers::governance::GovernanceController; +use crate::controllers::home::HomeController; +use crate::controllers::marketplace::MarketplaceController; +use crate::controllers::ticket::TicketController; +use crate::middleware::JwtAuth; +use actix_session::{SessionMiddleware, storage::CookieSessionStore}; +use actix_web::web; /// Configures all application routes pub fn configure_routes(cfg: &mut web::ServiceConfig) { // Configure session middleware with the consistent key - let session_middleware = SessionMiddleware::builder( - CookieSessionStore::default(), - SESSION_KEY.clone() - ) - .cookie_secure(false) // Set to true in production with HTTPS - .build(); + let session_middleware = + SessionMiddleware::builder(CookieSessionStore::default(), SESSION_KEY.clone()) + .cookie_secure(false) // Set to true in production with HTTPS + .build(); // Public routes that don't require authentication cfg.service( @@ -33,56 +31,98 @@ pub fn configure_routes(cfg: &mut web::ServiceConfig) { .route("/about", web::get().to(HomeController::about)) .route("/contact", web::get().to(HomeController::contact)) .route("/contact", web::post().to(HomeController::submit_contact)) - // Auth routes .route("/login", web::get().to(AuthController::login_page)) .route("/login", web::post().to(AuthController::login)) .route("/register", web::get().to(AuthController::register_page)) .route("/register", web::post().to(AuthController::register)) .route("/logout", web::get().to(AuthController::logout)) - // Protected routes that require authentication // These routes will be protected by the JwtAuth middleware in the main.rs file .route("/editor", web::get().to(HomeController::editor)) - // Ticket routes .route("/tickets", web::get().to(TicketController::list_tickets)) .route("/tickets/new", web::get().to(TicketController::new_ticket)) .route("/tickets", web::post().to(TicketController::create_ticket)) - .route("/tickets/{id}", web::get().to(TicketController::show_ticket)) - .route("/tickets/{id}/comment", web::post().to(TicketController::add_comment)) - .route("/tickets/{id}/status/{status}", web::post().to(TicketController::update_status)) + .route( + "/tickets/{id}", + web::get().to(TicketController::show_ticket), + ) + .route( + "/tickets/{id}/comment", + web::post().to(TicketController::add_comment), + ) + .route( + "/tickets/{id}/status/{status}", + web::post().to(TicketController::update_status), + ) .route("/my-tickets", web::get().to(TicketController::my_tickets)) - // Calendar routes .route("/calendar", web::get().to(CalendarController::calendar)) - .route("/calendar/events/new", web::get().to(CalendarController::new_event)) - .route("/calendar/events", web::post().to(CalendarController::create_event)) - .route("/calendar/events/{id}/delete", web::post().to(CalendarController::delete_event)) - + .route( + "/calendar/events/new", + web::get().to(CalendarController::new_event), + ) + .route( + "/calendar/events", + web::post().to(CalendarController::create_event), + ) + .route( + "/calendar/events/{id}/delete", + web::post().to(CalendarController::delete_event), + ) // Governance routes .route("/governance", web::get().to(GovernanceController::index)) - .route("/governance/proposals", web::get().to(GovernanceController::proposals)) - .route("/governance/proposals/{id}", web::get().to(GovernanceController::proposal_detail)) - .route("/governance/proposals/{id}/vote", web::post().to(GovernanceController::submit_vote)) - .route("/governance/create", web::get().to(GovernanceController::create_proposal_form)) - .route("/governance/create", web::post().to(GovernanceController::submit_proposal)) - .route("/governance/my-votes", web::get().to(GovernanceController::my_votes)) - + .route( + "/governance/proposals", + web::get().to(GovernanceController::proposals), + ) + .route( + "/governance/proposals/{id}", + web::get().to(GovernanceController::proposal_detail), + ) + .route( + "/governance/proposals/{id}/vote", + web::post().to(GovernanceController::submit_vote), + ) + .route( + "/governance/create", + web::get().to(GovernanceController::create_proposal_form), + ) + .route( + "/governance/create", + web::post().to(GovernanceController::submit_proposal), + ) + .route( + "/governance/my-votes", + web::get().to(GovernanceController::my_votes), + ) + .route( + "/governance/activities", + web::get().to(GovernanceController::all_activities), + ) // Flow routes .service( web::scope("/flows") .route("", web::get().to(FlowController::index)) .route("/list", web::get().to(FlowController::list_flows)) .route("/{id}", web::get().to(FlowController::flow_detail)) - .route("/{id}/advance", web::post().to(FlowController::advance_flow_step)) - .route("/{id}/stuck", web::post().to(FlowController::mark_flow_step_stuck)) - .route("/{id}/step/{step_id}/log", web::post().to(FlowController::add_log_to_flow_step)) + .route( + "/{id}/advance", + web::post().to(FlowController::advance_flow_step), + ) + .route( + "/{id}/stuck", + web::post().to(FlowController::mark_flow_step_stuck), + ) + .route( + "/{id}/step/{step_id}/log", + web::post().to(FlowController::add_log_to_flow_step), + ) .route("/create", web::get().to(FlowController::create_flow_form)) .route("/create", web::post().to(FlowController::create_flow)) - .route("/my-flows", web::get().to(FlowController::my_flows)) + .route("/my-flows", web::get().to(FlowController::my_flows)), ) - // Contract routes .service( web::scope("/contracts") @@ -91,9 +131,8 @@ pub fn configure_routes(cfg: &mut web::ServiceConfig) { .route("/my", web::get().to(ContractController::my_contracts)) .route("/{id}", web::get().to(ContractController::detail)) .route("/create", web::get().to(ContractController::create_form)) - .route("/create", web::post().to(ContractController::create)) + .route("/create", web::post().to(ContractController::create)), ) - // Asset routes .service( web::scope("/assets") @@ -104,35 +143,72 @@ pub fn configure_routes(cfg: &mut web::ServiceConfig) { .route("/create", web::post().to(AssetController::create)) .route("/test", web::get().to(AssetController::test)) .route("/{id}", web::get().to(AssetController::detail)) - .route("/{id}/valuation", web::post().to(AssetController::add_valuation)) - .route("/{id}/transaction", web::post().to(AssetController::add_transaction)) - .route("/{id}/status/{status}", web::post().to(AssetController::update_status)) + .route( + "/{id}/valuation", + web::post().to(AssetController::add_valuation), + ) + .route( + "/{id}/transaction", + web::post().to(AssetController::add_transaction), + ) + .route( + "/{id}/status/{status}", + web::post().to(AssetController::update_status), + ), ) - // Marketplace routes .service( web::scope("/marketplace") .route("", web::get().to(MarketplaceController::index)) - .route("/listings", web::get().to(MarketplaceController::list_listings)) + .route( + "/listings", + web::get().to(MarketplaceController::list_listings), + ) .route("/my", web::get().to(MarketplaceController::my_listings)) - .route("/create", web::get().to(MarketplaceController::create_listing_form)) - .route("/create", web::post().to(MarketplaceController::create_listing)) - .route("/{id}", web::get().to(MarketplaceController::listing_detail)) - .route("/{id}/bid", web::post().to(MarketplaceController::submit_bid)) - .route("/{id}/purchase", web::post().to(MarketplaceController::purchase_listing)) - .route("/{id}/cancel", web::post().to(MarketplaceController::cancel_listing)) + .route( + "/create", + web::get().to(MarketplaceController::create_listing_form), + ) + .route( + "/create", + web::post().to(MarketplaceController::create_listing), + ) + .route( + "/{id}", + web::get().to(MarketplaceController::listing_detail), + ) + .route( + "/{id}/bid", + web::post().to(MarketplaceController::submit_bid), + ) + .route( + "/{id}/purchase", + web::post().to(MarketplaceController::purchase_listing), + ) + .route( + "/{id}/cancel", + web::post().to(MarketplaceController::cancel_listing), + ), ) - // DeFi routes .service( web::scope("/defi") .route("", web::get().to(DefiController::index)) - .route("/providing", web::post().to(DefiController::create_providing)) - .route("/receiving", web::post().to(DefiController::create_receiving)) + .route( + "/providing", + web::post().to(DefiController::create_providing), + ) + .route( + "/receiving", + web::post().to(DefiController::create_receiving), + ) .route("/liquidity", web::post().to(DefiController::add_liquidity)) .route("/staking", web::post().to(DefiController::create_staking)) .route("/swap", web::post().to(DefiController::swap_tokens)) - .route("/collateral", web::post().to(DefiController::create_collateral)) + .route( + "/collateral", + web::post().to(DefiController::create_collateral), + ), ) // Company routes .service( @@ -140,13 +216,15 @@ pub fn configure_routes(cfg: &mut web::ServiceConfig) { .route("", web::get().to(CompanyController::index)) .route("/register", web::post().to(CompanyController::register)) .route("/view/{id}", web::get().to(CompanyController::view_company)) - .route("/switch/{id}", web::get().to(CompanyController::switch_entity)) - ) + .route( + "/switch/{id}", + web::get().to(CompanyController::switch_entity), + ), + ), ); - + // Keep the /protected scope for any future routes that should be under that path cfg.service( - web::scope("/protected") - .wrap(JwtAuth) // Apply JWT authentication middleware + web::scope("/protected").wrap(JwtAuth), // Apply JWT authentication middleware ); -} \ No newline at end of file +} diff --git a/actix_mvc_app/src/utils/mod.rs b/actix_mvc_app/src/utils/mod.rs index 129cdbc..dcc2732 100644 --- a/actix_mvc_app/src/utils/mod.rs +++ b/actix_mvc_app/src/utils/mod.rs @@ -1,16 +1,17 @@ -use actix_web::{error, Error, HttpResponse}; +use actix_web::{Error, HttpResponse}; use chrono::{DateTime, Utc}; -use tera::{self, Context, Function, Tera, Value}; use std::error::Error as StdError; +use tera::{self, Context, Function, Tera, Value}; // Export modules pub mod redis_service; // Re-export for easier imports -pub use redis_service::RedisCalendarService; +// pub use redis_service::RedisCalendarService; // Currently unused /// Error type for template rendering #[derive(Debug)] +#[allow(dead_code)] pub struct TemplateError { pub message: String, pub details: String, @@ -25,10 +26,16 @@ impl std::fmt::Display for TemplateError { impl std::error::Error for TemplateError {} -/// Registers custom Tera functions +/// Registers custom Tera functions and filters pub fn register_tera_functions(tera: &mut tera::Tera) { tera.register_function("now", NowFunction); tera.register_function("format_date", FormatDateFunction); + tera.register_function("local_time", LocalTimeFunction); + + // Register custom filters + tera.register_filter("format_hour", format_hour_filter); + tera.register_filter("extract_hour", extract_hour_filter); + tera.register_filter("format_time", format_time_filter); } /// Tera function to get the current date/time @@ -46,7 +53,7 @@ impl Function for NowFunction { }; let now = Utc::now(); - + // Special case for just getting the year if args.get("year").and_then(|v| v.as_bool()).unwrap_or(false) { return Ok(Value::String(now.format("%Y").to_string())); @@ -68,14 +75,10 @@ impl Function for FormatDateFunction { None => { return Err(tera::Error::msg( "The 'timestamp' argument must be a valid timestamp", - )) + )); } }, - None => { - return Err(tera::Error::msg( - "The 'timestamp' argument is required", - )) - } + None => return Err(tera::Error::msg("The 'timestamp' argument is required")), }; let format = match args.get("format") { @@ -89,23 +92,130 @@ impl Function for FormatDateFunction { // Convert timestamp to DateTime using the non-deprecated method let datetime = match DateTime::from_timestamp(timestamp, 0) { Some(dt) => dt, - None => { - return Err(tera::Error::msg( - "Failed to convert timestamp to datetime", - )) - } + None => return Err(tera::Error::msg("Failed to convert timestamp to datetime")), }; - + Ok(Value::String(datetime.format(format).to_string())) } } +/// Tera function to convert UTC datetime to local time +#[derive(Clone)] +pub struct LocalTimeFunction; + +impl Function for LocalTimeFunction { + fn call(&self, args: &std::collections::HashMap) -> tera::Result { + 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 + // 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, +) -> tera::Result { + 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, +) -> tera::Result { + 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, +) -> tera::Result { + let format = match args.get("format") { + Some(val) => match val.as_str() { + Some(s) => s, + None => "%H:%M", + }, + None => "%H:%M", + }; + + match value.as_str() { + Some(datetime_str) => { + // Try to parse as RFC3339 first + if let Ok(dt) = DateTime::parse_from_rfc3339(datetime_str) { + Ok(Value::String(dt.format(format).to_string())) + } else { + // Try to parse as our standard format + match DateTime::parse_from_str(datetime_str, "%Y-%m-%d %H:%M:%S%.f UTC") { + Ok(dt) => Ok(Value::String(dt.format(format).to_string())), + Err(_) => Err(tera::Error::msg("Invalid datetime string format")), + } + } + } + None => Err(tera::Error::msg("Value must be a string")), + } +} + /// Formats a date for display +#[allow(dead_code)] pub fn format_date(date: &DateTime, format: &str) -> String { date.format(format).to_string() } /// Truncates a string to a maximum length and adds an ellipsis if truncated +#[allow(dead_code)] pub fn truncate_string(s: &str, max_length: usize) -> String { if s.len() <= max_length { s.to_string() @@ -124,38 +234,41 @@ pub fn render_template( ctx: &Context, ) -> Result { println!("DEBUG: Attempting to render template: {}", template_name); - + // Print all context keys for debugging let mut keys = Vec::new(); for (key, _) in ctx.clone().into_json().as_object().unwrap().iter() { keys.push(key.clone()); } println!("DEBUG: Context keys: {:?}", keys); - + match tmpl.render(template_name, ctx) { Ok(content) => { println!("DEBUG: Successfully rendered template: {}", template_name); Ok(HttpResponse::Ok().content_type("text/html").body(content)) - }, + } Err(e) => { // Log the error with more details - println!("DEBUG: Template rendering error for {}: {}", template_name, e); + println!( + "DEBUG: Template rendering error for {}: {}", + template_name, e + ); println!("DEBUG: Error details: {:?}", e); - + // Print the error cause chain for better debugging let mut current_error: Option<&dyn StdError> = Some(&e); let mut error_chain = Vec::new(); - + while let Some(error) = current_error { error_chain.push(format!("{}", error)); current_error = error.source(); } - + println!("DEBUG: Error chain: {:?}", error_chain); - + // Log the error log::error!("Template rendering error: {}", e); - + // Create a simple error response with more detailed information let error_html = format!( r#" @@ -187,9 +300,9 @@ pub fn render_template( e, error_chain.join("\n") ); - + println!("DEBUG: Returning simple error page"); - + Ok(HttpResponse::InternalServerError() .content_type("text/html") .body(error_html)) @@ -207,4 +320,4 @@ mod tests { assert_eq!(truncate_string("Hello, world!", 5), "Hello..."); assert_eq!(truncate_string("", 5), ""); } -} \ No newline at end of file +} diff --git a/actix_mvc_app/src/utils/redis_service.rs b/actix_mvc_app/src/utils/redis_service.rs index f7244a4..76d89f5 100644 --- a/actix_mvc_app/src/utils/redis_service.rs +++ b/actix_mvc_app/src/utils/redis_service.rs @@ -1,7 +1,7 @@ +use heromodels::models::Event as CalendarEvent; +use lazy_static::lazy_static; use redis::{Client, Commands, Connection, RedisError}; use std::sync::{Arc, Mutex}; -use lazy_static::lazy_static; -use crate::models::CalendarEvent; // Create a lazy static Redis client that can be used throughout the application lazy_static! { @@ -11,21 +11,21 @@ lazy_static! { /// Initialize the Redis client pub fn init_redis_client(redis_url: &str) -> Result<(), RedisError> { let client = redis::Client::open(redis_url)?; - + // Test the connection let _: Connection = client.get_connection()?; - + // Store the client in the lazy static let mut client_guard = REDIS_CLIENT.lock().unwrap(); *client_guard = Some(client); - + Ok(()) } /// Get a Redis connection pub fn get_connection() -> Result { let client_guard = REDIS_CLIENT.lock().unwrap(); - + if let Some(client) = &*client_guard { client.get_connection() } else { @@ -42,14 +42,14 @@ pub struct RedisCalendarService; impl RedisCalendarService { /// Key prefix for calendar events const EVENT_KEY_PREFIX: &'static str = "calendar:event:"; - + /// Key for the set of all event IDs const ALL_EVENTS_KEY: &'static str = "calendar:all_events"; - + /// Save a calendar event to Redis pub fn save_event(event: &CalendarEvent) -> Result<(), RedisError> { let mut conn = get_connection()?; - + // Convert the event to JSON let json = event.to_json().map_err(|e| { RedisError::from(std::io::Error::new( @@ -57,25 +57,25 @@ impl RedisCalendarService { format!("Failed to serialize event: {}", e), )) })?; - + // Save the event - let event_key = format!("{}{}", Self::EVENT_KEY_PREFIX, event.id); + let event_key = format!("{}{}", Self::EVENT_KEY_PREFIX, event.base_data.id); let _: () = conn.set(event_key, json)?; - + // Add the event ID to the set of all events - let _: () = conn.sadd(Self::ALL_EVENTS_KEY, &event.id)?; - + let _: () = conn.sadd(Self::ALL_EVENTS_KEY, &event.base_data.id)?; + Ok(()) } - + /// Get a calendar event from Redis by ID pub fn get_event(id: &str) -> Result, RedisError> { let mut conn = get_connection()?; - + // Get the event JSON let event_key = format!("{}{}", Self::EVENT_KEY_PREFIX, id); let json: Option = conn.get(event_key)?; - + // Parse the JSON if let Some(json) = json { let event = CalendarEvent::from_json(&json).map_err(|e| { @@ -84,34 +84,34 @@ impl RedisCalendarService { format!("Failed to deserialize event: {}", e), )) })?; - + Ok(Some(event)) } else { Ok(None) } } - + /// Delete a calendar event from Redis pub fn delete_event(id: &str) -> Result { let mut conn = get_connection()?; - + // Delete the event let event_key = format!("{}{}", Self::EVENT_KEY_PREFIX, id); let deleted: i32 = conn.del(event_key)?; - + // Remove the event ID from the set of all events let _: () = conn.srem(Self::ALL_EVENTS_KEY, id)?; - + Ok(deleted > 0) } - + /// Get all calendar events from Redis pub fn get_all_events() -> Result, RedisError> { let mut conn = get_connection()?; - + // Get all event IDs let event_ids: Vec = conn.smembers(Self::ALL_EVENTS_KEY)?; - + // Get all events let mut events = Vec::new(); for id in event_ids { @@ -119,23 +119,23 @@ impl RedisCalendarService { events.push(event); } } - + Ok(events) } - + /// Get events for a specific date range pub fn get_events_in_range( start: chrono::DateTime, end: chrono::DateTime, ) -> Result, RedisError> { let all_events = Self::get_all_events()?; - + // Filter events that fall within the date range let filtered_events = all_events .into_iter() .filter(|event| event.start_time <= end && event.end_time >= start) .collect(); - + Ok(filtered_events) } -} \ No newline at end of file +} diff --git a/actix_mvc_app/src/views/calendar/index.html b/actix_mvc_app/src/views/calendar/index.html index fcca452..4fe1f1f 100644 --- a/actix_mvc_app/src/views/calendar/index.html +++ b/actix_mvc_app/src/views/calendar/index.html @@ -4,129 +4,1187 @@ {% block content %}
-

Calendar

- -

View Mode: {{ view_mode }}

-

Current Date: {{ current_date }}

- - - - {% if view_mode == "month" %} -

Month View: {{ month_name }} {{ current_year }}

- - - - - - - - - - - - - - - {% for week in range(start=0, end=6) %} - - {% for day_idx in range(start=0, end=7) %} - - {% endfor %} - - {% endfor %} - -
SunMonTueWedThuFriSat
- {% set idx = week * 7 + day_idx %} - {% if idx < calendar_days|length %} - {% set day = calendar_days[idx] %} - {% if day.day > 0 %} - {{ day.day }} - {% endif %} - {% endif %} -
- {% elif view_mode == "year" %} -

Year View: {{ current_year }}

- -
- {% for month in months %} -
-
-
{{ month.name }}
-
-

Events: {{ month.events|length }}

-
-
+ +
+
+
+
+

Calendar

+

Manage your events and schedule

+
+
+
- {% endfor %} -
- {% elif view_mode == "day" %} -

Day View: {{ current_date }}

- -
-
- All Day Events -
-
- {% if events is defined and events|length > 0 %} - {% for event in events %} - {% if event.all_day %} -
-
{{ event.title }}
-

{{ event.description }}

-
- {% endif %} - {% endfor %} - {% else %} -

No all-day events

- {% endif %}
- -
- {% for hour in range(start=0, end=24) %} -
-
-
- {{ "%02d"|format(value=hour) }}:00 -
-
- {% if events is defined and events|length > 0 %} - {% for event in events %} - {% if not event.all_day %} - {% set start_hour = event.start_time|date(format="%H") %} - {% if start_hour == hour|string %} -
-
{{ event.title }}
-

{{ event.start_time|date(format="%H:%M") }} - {{ event.end_time|date(format="%H:%M") }}

-

{{ event.description }}

-
+
+ + +
+
+
+ +
+ {{ current_date }} +
+
+ {% if view_mode == "month" %} +
+ + Use arrow keys to navigate months + | Click on any day to create an event + +
+ {% endif %} +
+
+ + {% if view_mode == "month" %} + +
+
+
+ +

+ {{ month_name }} {{ current_year }} +

+ +
+
+
+
+ + + + + + + + + + + + + + {% for week in range(start=0, end=6) %} + + {% for day_idx in range(start=0, end=7) %} + {% set idx = week * 7 + day_idx %} + + {% endfor %} + + {% endfor %} + +
SundayMondayTuesdayWednesdayThursdayFridaySaturday
+ {% if idx < calendar_days|length %} {% set day=calendar_days[idx] %} {% if day.day> 0 %} +
+ + {{ day.day }} + + {% if day.events|length > 0 %} + {{ day.events|length }} {% endif %} +
+ {% if day.events|length > 0 %} +
+ {% for event in day.events %} +
+ {{ event.title }} +
+ {% endfor %} + {% if day.events|length > 2 %} + + {% endif %} +
{% endif %} - {% endfor %} + {% endif %} + {% endif %} +
+
+
+
+ {% elif view_mode == "year" %} + +
+
+

+ Year {{ current_year }} +

+
+
+
+ {% for month in months %} +
+
+
+
{{ month.name }}
+
+
+ {% if month.events|length > 0 %} +
+ {{ month.events|length }} +
+

+ {% if month.events|length == 1 %} + 1 event + {% else %} + {{ month.events|length }} events + {% endif %} +

+ + {% else %} +
+ +

No events

+
{% endif %}
- {% endfor %} + {% endfor %} +
+
+ {% elif view_mode == "day" %} +

Day View: {{ current_date }}

+ +
+
+ All Day Events +
+
+ {% if events is defined and events|length > 0 %} + {% set has_all_day_events = false %} + {% for event in events %} + {% if event.all_day %} + {% set_global has_all_day_events = true %} +
+
{{ event.title }}
+ {% if event.description %} +

{{ event.description }}

+ {% endif %} +
+ {% endif %} + {% endfor %} + {% if not has_all_day_events %} +

No all-day events for this date

+ {% endif %} + {% else %} +

No events for this date

+ {% endif %} +
+
+ +
+ {% for hour in range(start=0, end=24) %} +
+
+
+ {{ hour|format_hour }}:00 +
+
+ {% if events is defined and events|length > 0 %} + {% for event in events %} + {% if not event.all_day %} + {% set start_hour = event.start_time|extract_hour %} + {% if start_hour == hour %} +
+
{{ event.title }}
+

{{ event.start_time|format_time }} - {{ event.end_time|format_time }}

+

{{ event.description }}

+
+ {% endif %} + {% endif %} + {% endfor %} + {% endif %} +
+
+
+ {% endfor %} +
{% endif %} - - - + + + + + + + + + + + +
{% block extra_css %} + +{% endblock %} + +{% block extra_js %} + {% endblock %} {% endblock %} \ No newline at end of file diff --git a/actix_mvc_app/src/views/calendar/new_event.html b/actix_mvc_app/src/views/calendar/new_event.html index cfcc9c8..4b2646a 100644 --- a/actix_mvc_app/src/views/calendar/new_event.html +++ b/actix_mvc_app/src/views/calendar/new_event.html @@ -5,29 +5,29 @@ {% block content %}

Create New Event

- + {% if error %} {% endif %} - -
+ +
- +
- +
- +
@@ -38,7 +38,14 @@
- + + + +
- +
Cancel @@ -59,37 +66,106 @@
{% endblock %} + +{% endblock %} \ No newline at end of file diff --git a/actix_mvc_app/src/views/governance/index.html b/actix_mvc_app/src/views/governance/index.html index 6ca829e..0a8a3c9 100644 --- a/actix_mvc_app/src/views/governance/index.html +++ b/actix_mvc_app/src/views/governance/index.html @@ -3,170 +3,192 @@ {% block title %}Governance Dashboard{% endblock %} {% block content %} - -
-
- + +{% include "governance/_header.html" %} + + +{% include "governance/_tabs.html" %} + + +
+
+
+ +
About Governance
+

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.

+
+
- -
-
-
- -
About Governance
-

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.

-
- Read Documentation + +
+ +
+ {% if nearest_proposal is defined %} +
+
+
Urgent: Voting Closes Soon
+
+ Ends: {{ nearest_proposal.vote_end_date | + date(format="%Y-%m-%d") }} + View Full Proposal +
+
+
+

{{ nearest_proposal.title }}

+
Proposed by {{ nearest_proposal.creator_name }}
+ +
+

{{ nearest_proposal.description }}

+
+ + {% 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 %} + +
+
{{ yes_percent }}% Yes
+
{{ no_percent }}% No +
+
{{ abstain_percent + }}% Abstain +
+
+ +
+ {{ total_votes }} votes cast + Quorum: {% if total_votes >= 20 %}75% reached{% else %}Not reached{% endif %} +
+ +
+
Cast Your Vote
+ +
+ +
+
+ + + +
+
- - -
- -
- {% if nearest_proposal is defined %} -
-
-
Urgent: Voting Closes Soon
-
- Ends: {{ nearest_proposal.voting_ends_at | date(format="%Y-%m-%d") }} - View Full Proposal -
-
-
-

{{ nearest_proposal.title }}

-
Proposed by {{ nearest_proposal.creator_name }}
- -
-

{{ nearest_proposal.description }}

-
- -
-
65% Yes
-
35% No
-
- -
- 26 votes cast - Quorum: 75% reached -
- -
-
Cast Your Vote
-
-
- -
-
- - - -
-
-
-
+ {% else %} +
+
+ +
No active proposals requiring votes
+

When new proposals are created, they will appear here for voting.

+ Create Proposal
- {% else %} -
-
- -
No active proposals requiring votes
-

When new proposals are created, they will appear here for voting.

- Create Proposal -
-
- {% endif %}
- - -
-
-
-
Recent Activity
-
-
-
- {% for activity in recent_activity %} -
-
-
- -
-
-
- {{ activity.user }} - {{ activity.timestamp | date(format="%H:%M") }} -
-

{{ activity.action }} on {{ activity.proposal_title }}

- {% if activity.type == "comment" and activity.comment is defined %} -

"{{ activity.comment }}"

- {% endif %} + {% endif %} +
+ + +
+
+
+
Recent Activity
+
+
+
+ {% for activity in recent_activity %} +
+
+
+ +
+
+
+ {{ activity.user }} + {{ activity.created_at | date(format="%H:%M") }}
+

{{ activity.action }} on {{ + activity.proposal_title }}

+ {% if activity.type == "comment" and activity.comment is defined %} +

"{{ activity.comment }}"

+ {% endif %}
- {% endfor %}
+ {% endfor %}
- +
+
+
- -
-
-
-
-
Active Proposals (Ending Soon)
-
-
-
- {% set count = 0 %} - {% for proposal in proposals %} - {% if count < 3 %} -
-
-
-
{{ proposal.title }}
-
By {{ proposal.creator_name }}
-

{{ proposal.description | truncate(length=100) }}

-
- - {{ proposal.status }} - - View Details -
-
- -
+ +
+
+
+
+
Active Proposals (Ending Soon)
+
+
+
+ {% set count = 0 %} + {% for proposal in proposals %} + {% if count < 3 %}
+
+
+
{{ proposal.title }}
+
By {{ proposal.creator_name }}
+

{{ proposal.description | truncate(length=100) }}

+
+ + {{ proposal.status }} + + View Details
- {% set count = count + 1 %} - {% endif %} - {% endfor %} -
+
+ +
+ {% set count = count + 1 %} + {% endif %} + {% endfor %}
-{% endblock %} +
+
+{% endblock %} \ No newline at end of file diff --git a/actix_mvc_app/src/views/governance/my_votes.html b/actix_mvc_app/src/views/governance/my_votes.html index 626d250..975d8b1 100644 --- a/actix_mvc_app/src/views/governance/my_votes.html +++ b/actix_mvc_app/src/views/governance/my_votes.html @@ -3,133 +3,121 @@ {% block title %}My Votes - Governance Dashboard{% endblock %} {% block content %} - -
-
- -
-
+ +{% include "governance/_header.html" %} - -
-
-
-
-
My Voting History
-
-
- {% if votes | length > 0 %} -
- - - - - - - - - - - - {% for item in votes %}{% set vote = item.0 %}{% set proposal = item.1 %} - - - - - - - - {% endfor %} - -
ProposalMy VoteStatusVoted OnActions
{{ proposal.title }} - - {{ vote.vote_type }} - - - - {{ proposal.status }} - - {{ vote.created_at | date(format="%Y-%m-%d") }} - View Proposal -
-
- {% else %} -
- -
You haven't voted on any proposals yet
-

When you vote on proposals, they will appear here.

- Browse Proposals -
- {% endif %} -
-
-
-
+ +{% include "governance/_tabs.html" %} - - {% if votes | length > 0 %} -
-
-
-
-
Yes Votes
-

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

-
-
-
-
-
-
-
No Votes
-

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

-
-
-
-
-
-
-
Abstain Votes
-

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

-
+ +
+
+
+ +
About Votes
+

Voting is a fundamental right of all token holders in our governance system. Each vote carries weight + proportional to your token holdings, ensuring fair representation. The voting statistics below show the + community's collective decision-making across all proposals.

+
- {% endif %} -{% endblock %} +
+ + +
+
+
+
+
Yes Votes
+

+ {{ total_yes_votes }} +

+
+
+
+
+
+
+
No Votes
+

+ {{ total_no_votes }} +

+
+
+
+
+
+
+
Abstain Votes
+

+ {{ total_abstain_votes }} +

+
+
+
+
+ + +
+
+
+
+
My Voting History
+
+
+ {% if votes | length > 0 %} +
+ + + + + + + + + + + + {% for item in votes %}{% set vote = item.0 %}{% set proposal = item.1 %} + + + + + + + + {% endfor %} + +
ProposalMy VoteStatusVoted OnActions
{{ proposal.title }} + + {{ vote.vote_type }} + + + + {{ proposal.status }} + + {{ vote.created_at | date(format="%Y-%m-%d") }} + View Proposal +
+
+ {% else %} +
+ +
You haven't voted on any proposals yet
+

When you vote on proposals, they will appear here.

+ Browse Proposals +
+ {% endif %} +
+
+
+
+ + +{% endblock %} \ No newline at end of file diff --git a/actix_mvc_app/src/views/governance/proposal_detail.html b/actix_mvc_app/src/views/governance/proposal_detail.html index e2506c5..a52cb06 100644 --- a/actix_mvc_app/src/views/governance/proposal_detail.html +++ b/actix_mvc_app/src/views/governance/proposal_detail.html @@ -2,8 +2,45 @@ {% block title %}{{ proposal.title }} - Governance Proposal{% endblock %} +{% block styles %} + +{% endblock %} + {% block content %}
+ + {% include "governance/_header.html" %} + + + {% include "governance/_tabs.html" %} +