From e4e403e2313a311f7e1e7d859f96e6c1af5f054f Mon Sep 17 00:00:00 2001 From: Mahmoud Emad Date: Sun, 18 May 2025 09:07:59 +0300 Subject: [PATCH 01/26] feat: Integerated the DB: - Added an initialization with the db - Implemented 'add_new_proposal' function to be used in the form --- actix_mvc_app/.cargo/config.toml | 2 + actix_mvc_app/Cargo.lock | 259 +++++++++++++++++++- actix_mvc_app/Cargo.toml | 7 + actix_mvc_app/src/controllers/governance.rs | 194 ++++++++++----- actix_mvc_app/src/db/mod.rs | 1 + actix_mvc_app/src/db/proposals.rs | 44 ++++ actix_mvc_app/src/main.rs | 1 + 7 files changed, 441 insertions(+), 67 deletions(-) create mode 100644 actix_mvc_app/.cargo/config.toml create mode 100644 actix_mvc_app/src/db/mod.rs create mode 100644 actix_mvc_app/src/db/proposals.rs 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..63aeda6 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,54 @@ 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 = "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", + "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 +1637,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 +1845,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 +1922,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 +1942,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 +2018,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "198db74531d58c70a361c42201efde7e2591e976d518caf7662a47dc5720e7b6" dependencies = [ "memchr", - "thiserror", + "thiserror 2.0.12", "ucd-trie", ] @@ -2210,6 +2321,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", + "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 +2427,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 +2611,7 @@ checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" dependencies = [ "num-bigint", "num-traits", - "thiserror", + "thiserror 2.0.12", "time", ] @@ -2456,6 +2646,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,6 +2689,12 @@ 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" @@ -2557,13 +2764,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 +2956,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 +3072,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 +3135,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/controllers/governance.rs b/actix_mvc_app/src/controllers/governance.rs index b485c00..be2b9f4 100644 --- a/actix_mvc_app/src/controllers/governance.rs +++ b/actix_mvc_app/src/controllers/governance.rs @@ -1,11 +1,14 @@ -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::proposals; +use crate::models::governance::{Proposal, ProposalStatus, Vote, VoteType, VotingResults}; use crate::utils::render_template; +use actix_session::Session; +use actix_web::{HttpResponse, Responder, Result, web}; +use chrono::{Duration, Utc}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use tera::Tera; + +use chrono::prelude::*; /// Controller for handling governance-related routes pub struct GovernanceController; @@ -15,10 +18,12 @@ impl GovernanceController { /// 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 @@ -37,38 +42,39 @@ impl GovernanceController { pub async fn index(tmpl: web::Data, 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); - + // Get mock proposals for the dashboard - let mut proposals = Self::get_mock_proposals(); - + let proposals = Self::get_mock_proposals(); + // Filter for active proposals only - let active_proposals: Vec = proposals.into_iter() + let active_proposals: Vec = proposals + .into_iter() .filter(|p| p.status == 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)); - + ctx.insert("proposals", &sorted_active_proposals); - + // Get the nearest deadline proposal for the voting pane if let Some(nearest_proposal) = sorted_active_proposals.first() { ctx.insert("nearest_proposal", nearest_proposal); } - + // 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(); ctx.insert("stats", &stats); - + render_template(&tmpl, "governance/index.html", &ctx) } @@ -77,72 +83,80 @@ impl GovernanceController { let mut ctx = tera::Context::new(); ctx.insert("active_page", "governance"); ctx.insert("active_tab", "proposals"); - + // 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(); ctx.insert("proposals", &proposals); - + 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 + tmpl: web::Data, + session: Session, ) -> Result { let proposal_id = path.into_inner(); 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); - + // 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)), + 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"); - + // 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,23 +164,66 @@ 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(); + + match proposals::create_new_proposal( + &user_id, + proposal_title, + proposal_description, + voting_start_date, + voting_end_date, + ) { + Ok((proposal_id, saved_proposal)) => { + println!( + "Proposal saved to DB: ID={}, title={:?}", + proposal_id, saved_proposal.title + ); + 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(); ctx.insert("proposals", &proposals); - + render_template(&tmpl, "governance/proposals.html", &ctx) } @@ -175,47 +232,54 @@ impl GovernanceController { path: web::Path, _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()); + 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)), + 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 + ))) } } } @@ -226,15 +290,15 @@ impl GovernanceController { let mut ctx = tera::Context::new(); ctx.insert("active_page", "governance"); ctx.insert("active_tab", "my_votes"); - + // 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); - + render_template(&tmpl, "governance/my_votes.html", &ctx) } @@ -301,7 +365,7 @@ impl GovernanceController { } // Mock data generation methods - + /// Generate mock proposals for testing fn get_mock_proposals() -> Vec { let now = Utc::now(); @@ -489,11 +553,13 @@ impl GovernanceController { updated_at: Utc::now() - Duration::days(5), }, ]; - + let proposals = Self::get_mock_proposals(); - votes.into_iter() + votes + .into_iter() .filter_map(|vote| { - proposals.iter() + proposals + .iter() .find(|p| p.id == vote.proposal_id) .map(|p| (vote.clone(), p.clone())) }) @@ -504,11 +570,11 @@ impl GovernanceController { 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); } - + results } diff --git a/actix_mvc_app/src/db/mod.rs b/actix_mvc_app/src/db/mod.rs new file mode 100644 index 0000000..8d18108 --- /dev/null +++ b/actix_mvc_app/src/db/mod.rs @@ -0,0 +1 @@ +pub mod proposals; diff --git a/actix_mvc_app/src/db/proposals.rs b/actix_mvc_app/src/db/proposals.rs new file mode 100644 index 0000000..130d332 --- /dev/null +++ b/actix_mvc_app/src/db/proposals.rs @@ -0,0 +1,44 @@ +use chrono::{Duration, Utc}; +use heromodels::db::hero::OurDB; +use heromodels::{ + db::{Collection, Db}, + models::governance::{Proposal, ProposalStatus}, +}; + +/// The path to the database file. Change this as needed for your environment. +pub const DB_PATH: &str = "/tmp/ourdb_governance"; + +/// 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(db_path: &str) -> Result { + let db = heromodels::db::hero::OurDB::new(db_path, true).expect("Can create DB"); + Ok(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, + title: &str, + description: &str, + voting_start_date: Option>, + voting_end_date: Option>, +) -> Result<(u32, Proposal), String> { + let db = get_db(DB_PATH).expect("Can create DB"); + + // Create a new proposal (with auto-generated ID) + let mut proposal = Proposal::new( + None, + creator_id, + title, + description, + voting_start_date.unwrap_or_else(Utc::now), + voting_end_date.unwrap_or_else(|| Utc::now() + Duration::days(7)), + ); + proposal.status = ProposalStatus::Draft; + + // 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)) +} diff --git a/actix_mvc_app/src/main.rs b/actix_mvc_app/src/main.rs index b129b76..a2e58be 100644 --- a/actix_mvc_app/src/main.rs +++ b/actix_mvc_app/src/main.rs @@ -8,6 +8,7 @@ use lazy_static::lazy_static; mod config; mod controllers; +mod db; mod middleware; mod models; mod routes; -- 2.40.1 From 60198dc2d4633b59840c8467ec60b6325fbce187 Mon Sep 17 00:00:00 2001 From: Mahmoud Emad Date: Sun, 18 May 2025 09:48:28 +0300 Subject: [PATCH 02/26] fix: Remove warnings --- actix_mvc_app/src/config/mod.rs | 8 +- actix_mvc_app/src/controllers/asset.rs | 639 +++++++++++++------ actix_mvc_app/src/controllers/auth.rs | 1 + actix_mvc_app/src/controllers/company.rs | 148 +++-- actix_mvc_app/src/controllers/contract.rs | 633 +++++++++++------- actix_mvc_app/src/controllers/defi.rs | 306 ++++++--- actix_mvc_app/src/controllers/flow.rs | 3 + actix_mvc_app/src/controllers/governance.rs | 2 + actix_mvc_app/src/controllers/home.rs | 1 + actix_mvc_app/src/controllers/marketplace.rs | 309 +++++---- actix_mvc_app/src/models/asset.rs | 1 + actix_mvc_app/src/models/contract.rs | 3 + actix_mvc_app/src/models/defi.rs | 3 + actix_mvc_app/src/models/flow.rs | 3 + actix_mvc_app/src/models/governance.rs | 3 + actix_mvc_app/src/models/marketplace.rs | 34 +- actix_mvc_app/src/models/mod.rs | 21 +- actix_mvc_app/src/models/ticket.rs | 1 + actix_mvc_app/src/models/user.rs | 4 + actix_mvc_app/src/utils/mod.rs | 52 +- 20 files changed, 1411 insertions(+), 764 deletions(-) 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/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 be2b9f4..8526ee6 100644 --- a/actix_mvc_app/src/controllers/governance.rs +++ b/actix_mvc_app/src/controllers/governance.rs @@ -13,6 +13,7 @@ use chrono::prelude::*; /// 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 @@ -607,6 +608,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, 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/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/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 index 1c4f1f3..486bef3 100644 --- a/actix_mvc_app/src/models/governance.rs +++ b/actix_mvc_app/src/models/governance.rs @@ -75,6 +75,7 @@ pub struct Proposal { pub voting_ends_at: Option>, } +#[allow(dead_code)] impl Proposal { /// Creates a new proposal pub fn new(creator_id: i32, creator_name: String, title: String, description: String) -> Self { @@ -140,6 +141,7 @@ pub struct Vote { pub updated_at: DateTime, } +#[allow(dead_code)] impl Vote { /// Creates a new vote pub fn new(proposal_id: String, voter_id: i32, voter_name: String, vote_type: VoteType, comment: Option) -> Self { @@ -200,6 +202,7 @@ pub struct VotingResults { pub total_votes: usize, } +#[allow(dead_code)] impl VotingResults { /// Creates a new empty voting results object pub fn new(proposal_id: String) -> Self { 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..d225c4b 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 governance; +pub mod marketplace; +pub mod ticket; +pub mod user; // Re-export models for easier imports -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}; +pub use defi::initialize_mock_data; +pub use ticket::{Ticket, TicketComment, TicketPriority, TicketStatus}; +pub use user::User; 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/utils/mod.rs b/actix_mvc_app/src/utils/mod.rs index 129cdbc..6bbaaa0 100644 --- a/actix_mvc_app/src/utils/mod.rs +++ b/actix_mvc_app/src/utils/mod.rs @@ -1,7 +1,7 @@ -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; @@ -11,6 +11,7 @@ pub use redis_service::RedisCalendarService; /// Error type for template rendering #[derive(Debug)] +#[allow(dead_code)] pub struct TemplateError { pub message: String, pub details: String, @@ -46,7 +47,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 +69,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 +86,21 @@ 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())) } } /// 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 +119,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 +185,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 +205,4 @@ mod tests { assert_eq!(truncate_string("Hello, world!", 5), "Hello..."); assert_eq!(truncate_string("", 5), ""); } -} \ No newline at end of file +} -- 2.40.1 From 4a2f1c72825378547c5df90011d854218f4988f9 Mon Sep 17 00:00:00 2001 From: Mahmoud-Emad Date: Wed, 21 May 2025 11:44:06 +0300 Subject: [PATCH 03/26] feat: Implement Proposals page: - Added the create new proposal functionality - Added the list all proposals functionnality --- actix_mvc_app/src/controllers/governance.rs | 229 +++++++++-------- actix_mvc_app/src/db/proposals.rs | 43 +++- .../src/views/governance/proposal_detail.html | 57 +++-- .../src/views/governance/proposals.html | 231 +++++++++--------- 4 files changed, 323 insertions(+), 237 deletions(-) diff --git a/actix_mvc_app/src/controllers/governance.rs b/actix_mvc_app/src/controllers/governance.rs index 8526ee6..7eaa772 100644 --- a/actix_mvc_app/src/controllers/governance.rs +++ b/actix_mvc_app/src/controllers/governance.rs @@ -1,9 +1,10 @@ use crate::db::proposals; -use crate::models::governance::{Proposal, ProposalStatus, Vote, VoteType, VotingResults}; +use crate::models::governance::{Vote, VoteType, VotingResults}; use crate::utils::render_template; use actix_session::Session; use actix_web::{HttpResponse, Responder, Result, web}; use chrono::{Duration, Utc}; +use heromodels::models::governance::{Proposal, ProposalStatus}; use serde::{Deserialize, Serialize}; use serde_json::Value; use tera::Tera; @@ -48,18 +49,24 @@ impl GovernanceController { let user = Self::get_user_from_session(&session).unwrap(); ctx.insert("user", &user); - // Get mock proposals for the dashboard - let proposals = Self::get_mock_proposals(); + // Get proposals from the database + let proposals = match crate::db::proposals::get_proposals() { + Ok(props) => props, + Err(e) => { + ctx.insert("error", &format!("Failed to load proposals: {}", e)); + vec![] + } + }; // Filter for active proposals only - let active_proposals: Vec = proposals + let active_proposals: Vec = proposals .into_iter() - .filter(|p| p.status == ProposalStatus::Active) + .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); @@ -90,8 +97,14 @@ impl GovernanceController { ctx.insert("user", &user); } - // Get mock proposals - let proposals = Self::get_mock_proposals(); + // Get proposals from the database + let proposals = match crate::db::proposals::get_proposals() { + Ok(props) => props, + Err(e) => { + ctx.insert("error", &format!("Failed to load proposals: {}", e)); + vec![] + } + }; ctx.insert("proposals", &proposals); render_template(&tmpl, "governance/proposals.html", &ctx) @@ -199,10 +212,24 @@ impl GovernanceController { .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 proposals::create_new_proposal( &user_id, + &user_name, proposal_title, proposal_description, + status, voting_start_date, voting_end_date, ) { @@ -221,8 +248,14 @@ impl GovernanceController { // For now, we'll just redirect to the proposals page with a success message - // Get mock proposals - let proposals = Self::get_mock_proposals(); + // Get proposals from the database + let proposals = match crate::db::proposals::get_proposals() { + Ok(props) => props, + Err(e) => { + ctx.insert("error", &format!("Failed to load proposals: {}", e)); + vec![] + } + }; ctx.insert("proposals", &proposals); render_template(&tmpl, "governance/proposals.html", &ctx) @@ -371,96 +404,98 @@ impl GovernanceController { 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, - }, + Proposal::new( + Some(1), + "1", + "Ibrahim Faraji", + "Establish Zanzibar Digital Trade Hub", + "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.", + ProposalStatus::Active, + now - Duration::days(5), + now - Duration::days(5), + now - Duration::days(3), + now + Duration::days(4), + ), + Proposal::new( + Some(2), + "2", + "Amina Salim", + "ZDFZ Sustainable Tourism Framework", + "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.", + ProposalStatus::Approved, + now - Duration::days(15), + now - Duration::days(2), + now - Duration::days(14), + now - Duration::days(2), + ), + Proposal::new( + Some(3), + "3", + "Hassan Mwinyi", + "Spice Industry Modernization Initiative", + "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.", + ProposalStatus::Draft, + now - Duration::days(1), + now - Duration::days(1), + now - Duration::days(1), + now + Duration::days(1), + ), + Proposal::new( + Some(4), + "4", + "Ibrahim Faraji", + "ZDFZ Regulatory Framework for Digital Financial Services", + "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.", + ProposalStatus::Rejected, + now - Duration::days(20), + now - Duration::days(5), + now - Duration::days(19), + now - Duration::days(5), + ), + Proposal::new( + Some(5), + "5", + "Fatma Busaidy", + "Digital Arts Incubator and Artwork Marketplace", + "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.", + ProposalStatus::Active, + now - Duration::days(7), + now - Duration::days(7), + now - Duration::days(6), + now + Duration::days(1), + ), + Proposal::new( + Some(6), + "6", + "Omar Makame", + "Zanzibar Renewable Energy Microgrid Network", + "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.", + ProposalStatus::Active, + now - Duration::days(10), + now - Duration::days(9), + now - Duration::days(8), + now + Duration::days(6), + ), + Proposal::new( + Some(7), + "7", + "Saida Juma", + "ZDFZ Educational Technology Initiative", + "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.", + ProposalStatus::Draft, + now - Duration::days(3), + now - Duration::days(2), + now - Duration::days(1), + now + Duration::days(1), + ), ] } /// 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) + Self::get_mock_proposals() + .into_iter() + .find(|p| p.base_data.id.to_string() == id) } /// Generate mock votes for a specific proposal @@ -561,7 +596,7 @@ impl GovernanceController { .filter_map(|vote| { proposals .iter() - .find(|p| p.id == vote.proposal_id) + .find(|p| p.base_data.id.to_string() == vote.proposal_id) .map(|p| (vote.clone(), p.clone())) }) .collect() @@ -600,6 +635,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 diff --git a/actix_mvc_app/src/db/proposals.rs b/actix_mvc_app/src/db/proposals.rs index 130d332..c2214ed 100644 --- a/actix_mvc_app/src/db/proposals.rs +++ b/actix_mvc_app/src/db/proposals.rs @@ -1,3 +1,5 @@ +use std::path::PathBuf; + use chrono::{Duration, Utc}; use heromodels::db::hero::OurDB; use heromodels::{ @@ -6,39 +8,70 @@ use heromodels::{ }; /// The path to the database file. Change this as needed for your environment. -pub const DB_PATH: &str = "/tmp/ourdb_governance"; +pub const DB_PATH: &str = "/tmp/ourdb_governance2"; /// 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(db_path: &str) -> Result { - let db = heromodels::db::hero::OurDB::new(db_path, true).expect("Can create DB"); + 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) } /// 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(DB_PATH).expect("Can create DB"); + let created_at = Utc::now(); + let updated_at = created_at; + // Create a new proposal (with auto-generated ID) - let mut proposal = Proposal::new( + 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)), ); - proposal.status = ProposalStatus::Draft; - // 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(DB_PATH).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) +} diff --git a/actix_mvc_app/src/views/governance/proposal_detail.html b/actix_mvc_app/src/views/governance/proposal_detail.html index e2506c5..b6e99ae 100644 --- a/actix_mvc_app/src/views/governance/proposal_detail.html +++ b/actix_mvc_app/src/views/governance/proposal_detail.html @@ -37,28 +37,30 @@
- + {{ proposal.status }} - Created by {{ proposal.creator_name }} on {{ proposal.created_at | date(format="%Y-%m-%d") }} + Created by {{ proposal.creator_name }} +
- +
Description

{{ proposal.description }}

- +
Voting Period

{% if proposal.voting_starts_at and proposal.voting_ends_at %} - Start: {{ proposal.voting_starts_at | date(format="%Y-%m-%d") }}
- End: {{ proposal.voting_ends_at | date(format="%Y-%m-%d") }} + Start: {{ proposal.voting_starts_at | date(format="%Y-%m-%d") }}
+ End: {{ proposal.voting_ends_at | date(format="%Y-%m-%d") }} {% else %} - Not set + Not set {% endif %}

- +
@@ -69,32 +71,35 @@ {% set yes_percent = 0 %} {% set no_percent = 0 %} {% set abstain_percent = 0 %} - + {% if results.total_votes > 0 %} - {% set yes_percent = (results.yes_count * 100 / results.total_votes) | int %} - {% set no_percent = (results.no_count * 100 / results.total_votes) | int %} - {% set abstain_percent = (results.abstain_count * 100 / results.total_votes) | int %} + {% set yes_percent = (results.yes_count * 100 / results.total_votes) | int %} + {% set no_percent = (results.no_count * 100 / results.total_votes) | int %} + {% set abstain_percent = (results.abstain_count * 100 / results.total_votes) | int %} {% endif %} - +

Yes: {{ results.yes_count }} ({{ yes_percent }}%)

-
+
+
- +

No: {{ results.no_count }} ({{ no_percent }}%)

-
+
+
- +

Abstain: {{ results.abstain_count }} ({{ abstain_percent }}%)

-
+

Total Votes: {{ results.total_votes }}

- + {% if proposal.status == "Active" and user and user.id %}
@@ -106,7 +111,8 @@
- + @@ -118,7 +124,8 @@
- + @@ -126,7 +133,8 @@
- +
@@ -167,7 +175,8 @@ {{ vote.voter_name }} - + {{ vote.vote_type }} @@ -186,4 +195,4 @@
-{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/actix_mvc_app/src/views/governance/proposals.html b/actix_mvc_app/src/views/governance/proposals.html index 36fa089..dba23f2 100644 --- a/actix_mvc_app/src/views/governance/proposals.html +++ b/actix_mvc_app/src/views/governance/proposals.html @@ -3,128 +3,135 @@ {% block title %}Proposals - Governance Dashboard{% endblock %} {% block content %} - - {% if success %} -
-
- -
-
- {% endif %} - - -
-
- -
-
- + +{% if success %} +
-
+
+
+{% endif %} - -
-
-
-
-
-
- - -
-
- - -
-
- -
-
-
-
+ +
+
+ +
+
+ +
+
+ +
About Proposals
+

Proposals are formal requests for changes to the platform that require community approval. Each proposal + includes a detailed description, implementation plan, and voting period. Browse the list below to see all + active and past proposals.

+
+
- -
-
-
-
-
All Proposals
- Create New Proposal -
-
-
- - - - - - - - - - - - - {% for proposal in proposals %} - - - - - - - - - {% endfor %} - -
TitleCreatorStatusCreatedVoting PeriodActions
{{ proposal.title }}{{ proposal.creator_name }} - - {{ proposal.status }} - - {{ proposal.created_at | date(format="%Y-%m-%d") }} - {% if proposal.voting_starts_at and proposal.voting_ends_at %} - {{ proposal.voting_starts_at | date(format="%Y-%m-%d") }} to {{ proposal.voting_ends_at | date(format="%Y-%m-%d") }} - {% else %} - Not set - {% endif %} - - View -
+ +
+
+
+
+
+
+ +
+
+ + +
+
+ +
+
+
+
+
+
+ + +
+
+
+
+
All Proposals
+ Create New Proposal +
+
+
+ + + + + + + + + + + + + {% for proposal in proposals %} + + + + + + + + + {% endfor %} + +
TitleCreatorStatusCreatedVoting PeriodActions
{{ proposal.title }}{{ proposal.creator_name }} + + {{ proposal.status }} + + {{ proposal.created_at | date(format="%Y-%m-%d") }} + {% if proposal.voting_starts_at and proposal.voting_ends_at %} + {{ proposal.voting_starts_at | date(format="%Y-%m-%d") }} to {{ + proposal.voting_ends_at | date(format="%Y-%m-%d") }} + {% else %} + Not set + {% endif %} + + View +
-{% endblock %} +
+{% endblock %} \ No newline at end of file -- 2.40.1 From 9c71c63ec5bdb09be928b9805315178457dca6f1 Mon Sep 17 00:00:00 2001 From: Mahmoud-Emad Date: Wed, 21 May 2025 12:21:38 +0300 Subject: [PATCH 04/26] feat: Working on the propsal page: - Integerated the view proposal detail db call - Use real data instead of mock data --- actix_mvc_app/src/controllers/governance.rs | 6 +- actix_mvc_app/src/db/proposals.rs | 15 ++ .../src/views/governance/my_votes.html | 243 +++++++++--------- .../src/views/governance/proposal_detail.html | 2 +- 4 files changed, 142 insertions(+), 124 deletions(-) diff --git a/actix_mvc_app/src/controllers/governance.rs b/actix_mvc_app/src/controllers/governance.rs index 7eaa772..e037aff 100644 --- a/actix_mvc_app/src/controllers/governance.rs +++ b/actix_mvc_app/src/controllers/governance.rs @@ -1,4 +1,4 @@ -use crate::db::proposals; +use crate::db::proposals::{self, get_proposal_by_id}; use crate::models::governance::{Vote, VoteType, VotingResults}; use crate::utils::render_template; use actix_session::Session; @@ -126,8 +126,8 @@ impl GovernanceController { } // 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 diff --git a/actix_mvc_app/src/db/proposals.rs b/actix_mvc_app/src/db/proposals.rs index c2214ed..425d530 100644 --- a/actix_mvc_app/src/db/proposals.rs +++ b/actix_mvc_app/src/db/proposals.rs @@ -75,3 +75,18 @@ pub fn get_proposals() -> Result, String> { }; 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(DB_PATH).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)) + } + } +} diff --git a/actix_mvc_app/src/views/governance/my_votes.html b/actix_mvc_app/src/views/governance/my_votes.html index 626d250..9a1085d 100644 --- a/actix_mvc_app/src/views/governance/my_votes.html +++ b/actix_mvc_app/src/views/governance/my_votes.html @@ -3,133 +3,136 @@ {% block title %}My Votes - Governance Dashboard{% endblock %} {% block content %} - -
-
- + +
+
+ +
+
+ + +
+
+
+
+
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 %} +
+
- -
-
-
-
-
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 -
+ +{% 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 }} +

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

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

- {% endif %} -{% endblock %} +
+
+
+
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 }} +

+
+
+
+
+{% 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 b6e99ae..98cd542 100644 --- a/actix_mvc_app/src/views/governance/proposal_detail.html +++ b/actix_mvc_app/src/views/governance/proposal_detail.html @@ -107,7 +107,7 @@
Cast Your Vote
-
+
-- 2.40.1 From 5d9eaac1f84f64efaf093a360ae012394a01ac4e Mon Sep 17 00:00:00 2001 From: Mahmoud-Emad Date: Wed, 21 May 2025 13:49:20 +0300 Subject: [PATCH 05/26] feat: Implemented submit vote --- actix_mvc_app/src/controllers/governance.rs | 109 +++++++++++------- actix_mvc_app/src/db/proposals.rs | 97 +++++++++++++++- .../src/views/governance/proposal_detail.html | 10 ++ 3 files changed, 172 insertions(+), 44 deletions(-) diff --git a/actix_mvc_app/src/controllers/governance.rs b/actix_mvc_app/src/controllers/governance.rs index e037aff..14c1c6c 100644 --- a/actix_mvc_app/src/controllers/governance.rs +++ b/actix_mvc_app/src/controllers/governance.rs @@ -134,8 +134,8 @@ impl GovernanceController { 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); + // Calculate voting results directly from the proposal + let results = Self::calculate_voting_results_from_proposal(&proposal); ctx.insert("results", &results); render_template(&tmpl, "governance/proposal_detail.html", &ctx) @@ -264,57 +264,62 @@ impl GovernanceController { /// 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, ) -> 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); - } + // 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); - // 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!"); + // Extract user ID + let user_id = user.get("id").and_then(|v| v.as_i64()).unwrap_or(1) as i32; - // Get mock votes for this proposal - let votes = Self::get_mock_votes_for_proposal(&proposal_id); - ctx.insert("votes", &votes); + // 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); + } + }; - // Get voting results - let results = Self::get_mock_voting_results(&proposal_id); - ctx.insert("results", &results); + // Submit the vote + match crate::db::proposals::submit_vote_on_proposal( + proposal_id_u32, + user_id, + &form.vote_type, + 1, // Default to 1 share + ) { + Ok(updated_proposal) => { + ctx.insert("proposal", &updated_proposal); + ctx.insert("success", "Your vote has been 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)), - Err(e) => { - eprintln!("Error rendering error template: {}", e); - Err(actix_web::error::ErrorInternalServerError(format!( - "Error: {}", - e - ))) - } + // Get votes for this proposal + // For now, we'll still use mock votes until we implement a function to extract votes from the proposal + let votes = Self::get_mock_votes_for_proposal(&proposal_id); + ctx.insert("votes", &votes); + + // Calculate voting results directly from the updated proposal + let results = Self::calculate_voting_results_from_proposal(&updated_proposal); + ctx.insert("results", &results); + + render_template(&tmpl, "governance/proposal_detail.html", &ctx) + } + Err(e) => { + ctx.insert("error", &format!("Failed to submit vote: {}", e)); + render_template(&tmpl, "error.html", &ctx) } } } @@ -614,6 +619,26 @@ impl GovernanceController { results } + /// 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()); + + // 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 { diff --git a/actix_mvc_app/src/db/proposals.rs b/actix_mvc_app/src/db/proposals.rs index 425d530..edc490f 100644 --- a/actix_mvc_app/src/db/proposals.rs +++ b/actix_mvc_app/src/db/proposals.rs @@ -4,11 +4,11 @@ use chrono::{Duration, Utc}; use heromodels::db::hero::OurDB; use heromodels::{ db::{Collection, Db}, - models::governance::{Proposal, ProposalStatus}, + models::governance::{Proposal, ProposalStatus, VoteEventStatus}, }; /// The path to the database file. Change this as needed for your environment. -pub const DB_PATH: &str = "/tmp/ourdb_governance2"; +pub const DB_PATH: &str = "/tmp/ourdb_governance3"; /// 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(db_path: &str) -> Result { @@ -90,3 +90,96 @@ pub fn get_proposal_by_id(proposal_id: u32) -> Result, String> } } } + +/// 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 +) -> Result { + // Get the proposal from the database + let db = get_db(DB_PATH).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; + + // We need to manually create a ballot since we can't use cast_vote + // This is a simplified version that just records the vote + println!( + "Recording vote: ballot_id={}, user_id={}, option_id={}, shares={}", + ballot_id, user_id, option_id, shares_count + ); + + // 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) +} diff --git a/actix_mvc_app/src/views/governance/proposal_detail.html b/actix_mvc_app/src/views/governance/proposal_detail.html index 98cd542..a4fba74 100644 --- a/actix_mvc_app/src/views/governance/proposal_detail.html +++ b/actix_mvc_app/src/views/governance/proposal_detail.html @@ -147,6 +147,16 @@ Login to Vote
+ {% elif proposal.status != "Active" %} +
+
+
+ + Note: Voting is only available for proposals with an Active status. + This proposal's current status is {{ proposal.status }}. +
+
+
{% endif %}
-- 2.40.1 From 916f435dbc65f539effc19d38f54ff5accf1d0ca Mon Sep 17 00:00:00 2001 From: Mahmoud-Emad Date: Wed, 21 May 2025 15:04:45 +0300 Subject: [PATCH 06/26] feat: Load voting --- actix_mvc_app/src/controllers/governance.rs | 101 ++++++++++++++++++-- actix_mvc_app/src/db/proposals.rs | 22 ++++- 2 files changed, 115 insertions(+), 8 deletions(-) diff --git a/actix_mvc_app/src/controllers/governance.rs b/actix_mvc_app/src/controllers/governance.rs index 14c1c6c..810e353 100644 --- a/actix_mvc_app/src/controllers/governance.rs +++ b/actix_mvc_app/src/controllers/governance.rs @@ -130,8 +130,8 @@ impl GovernanceController { 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); // Calculate voting results directly from the proposal @@ -300,15 +300,15 @@ impl GovernanceController { proposal_id_u32, user_id, &form.vote_type, - 1, // Default to 1 share + 1, // Default to 1 share + form.comment.as_ref().map(|s| s.to_string()), // Pass the comment from the form ) { Ok(updated_proposal) => { ctx.insert("proposal", &updated_proposal); ctx.insert("success", "Your vote has been recorded!"); - // Get votes for this proposal - // For now, we'll still use mock votes until we implement a function to extract votes from the proposal - let votes = Self::get_mock_votes_for_proposal(&proposal_id); + // Extract votes directly from the updated proposal + let votes = Self::extract_votes_from_proposal(&updated_proposal); ctx.insert("votes", &votes); // Calculate voting results directly from the updated proposal @@ -639,6 +639,95 @@ impl GovernanceController { results } + /// 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 + } + }; + + // Create a Vote from the ballot + let vote = Vote::new( + proposal.base_data.id.to_string(), + voter_id, + format!("User {}", voter_id), + vote_type, + ballot.comment.clone(), // Use the comment from the ballot + ); + + votes.push(vote); + } + + println!("Extracted {} votes from proposal", votes.len()); + votes + } + /// Generate mock statistics for the governance dashboard fn get_mock_statistics() -> GovernanceStats { GovernanceStats { diff --git a/actix_mvc_app/src/db/proposals.rs b/actix_mvc_app/src/db/proposals.rs index edc490f..042a358 100644 --- a/actix_mvc_app/src/db/proposals.rs +++ b/actix_mvc_app/src/db/proposals.rs @@ -4,11 +4,11 @@ use chrono::{Duration, Utc}; use heromodels::db::hero::OurDB; use heromodels::{ db::{Collection, Db}, - models::governance::{Proposal, ProposalStatus, VoteEventStatus}, + models::governance::{Proposal, ProposalStatus}, }; /// The path to the database file. Change this as needed for your environment. -pub const DB_PATH: &str = "/tmp/ourdb_governance3"; +pub const DB_PATH: &str = "/tmp/ourdb_governance6"; /// 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(db_path: &str) -> Result { @@ -97,6 +97,7 @@ pub fn submit_vote_on_proposal( 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(DB_PATH).map_err(|e| format!("DB error: {}", e))?; @@ -173,6 +174,23 @@ pub fn submit_vote_on_proposal( ballot_id, user_id, option_id, shares_count ); + // 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; + + // Add the ballot to the proposal's ballots + proposal.ballots.push(ballot); + // Update the proposal's updated_at timestamp proposal.updated_at = Utc::now(); -- 2.40.1 From 8f1438dc010ff1502d400e89e32ff0923e4df713 Mon Sep 17 00:00:00 2001 From: Mahmoud-Emad Date: Wed, 21 May 2025 15:43:17 +0300 Subject: [PATCH 07/26] feat: Remove mock proposals --- actix_mvc_app/src/controllers/governance.rs | 327 +++++--------------- 1 file changed, 76 insertions(+), 251 deletions(-) diff --git a/actix_mvc_app/src/controllers/governance.rs b/actix_mvc_app/src/controllers/governance.rs index 810e353..073b8e1 100644 --- a/actix_mvc_app/src/controllers/governance.rs +++ b/actix_mvc_app/src/controllers/governance.rs @@ -40,6 +40,43 @@ 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(); @@ -58,6 +95,9 @@ impl GovernanceController { } }; + // 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() @@ -75,14 +115,15 @@ impl GovernanceController { ctx.insert("nearest_proposal", nearest_proposal); } - // 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); + // For now, we'll use empty recent activity + // In a real application, this would be populated from a database + let recent_activity = Vec::::new(); + ctx.insert("recent_activity", &recent_activity); + render_template(&tmpl, "governance/index.html", &ctx) } @@ -334,174 +375,38 @@ impl GovernanceController { 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::proposals::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())); + } + } + } + + 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" - }), - ] - } - - // Mock data generation methods - - /// Generate mock proposals for testing - fn get_mock_proposals() -> Vec { - let now = Utc::now(); - vec![ - Proposal::new( - Some(1), - "1", - "Ibrahim Faraji", - "Establish Zanzibar Digital Trade Hub", - "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.", - ProposalStatus::Active, - now - Duration::days(5), - now - Duration::days(5), - now - Duration::days(3), - now + Duration::days(4), - ), - Proposal::new( - Some(2), - "2", - "Amina Salim", - "ZDFZ Sustainable Tourism Framework", - "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.", - ProposalStatus::Approved, - now - Duration::days(15), - now - Duration::days(2), - now - Duration::days(14), - now - Duration::days(2), - ), - Proposal::new( - Some(3), - "3", - "Hassan Mwinyi", - "Spice Industry Modernization Initiative", - "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.", - ProposalStatus::Draft, - now - Duration::days(1), - now - Duration::days(1), - now - Duration::days(1), - now + Duration::days(1), - ), - Proposal::new( - Some(4), - "4", - "Ibrahim Faraji", - "ZDFZ Regulatory Framework for Digital Financial Services", - "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.", - ProposalStatus::Rejected, - now - Duration::days(20), - now - Duration::days(5), - now - Duration::days(19), - now - Duration::days(5), - ), - Proposal::new( - Some(5), - "5", - "Fatma Busaidy", - "Digital Arts Incubator and Artwork Marketplace", - "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.", - ProposalStatus::Active, - now - Duration::days(7), - now - Duration::days(7), - now - Duration::days(6), - now + Duration::days(1), - ), - Proposal::new( - Some(6), - "6", - "Omar Makame", - "Zanzibar Renewable Energy Microgrid Network", - "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.", - ProposalStatus::Active, - now - Duration::days(10), - now - Duration::days(9), - now - Duration::days(8), - now + Duration::days(6), - ), - Proposal::new( - Some(7), - "7", - "Saida Juma", - "ZDFZ Educational Technology Initiative", - "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.", - ProposalStatus::Draft, - now - Duration::days(3), - now - Duration::days(2), - now - Duration::days(1), - now + Duration::days(1), - ), - ] - } - - /// Get a mock proposal by ID - fn get_mock_proposal_by_id(id: &str) -> Option { - Self::get_mock_proposals() - .into_iter() - .find(|p| p.base_data.id.to_string() == id) - } + // No more mock recent activity - we're using an empty vector in the index function /// Generate mock votes for a specific proposal fn get_mock_votes_for_proposal(proposal_id: &str) -> Vec { @@ -550,75 +455,6 @@ 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.base_data.id.to_string() == vote.proposal_id) - .map(|p| (vote.clone(), p.clone())) - }) - .collect() - } - - /// 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); - } - - results - } - /// 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()); @@ -728,18 +564,7 @@ impl GovernanceController { votes } - /// 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, - } - } + // The calculate_statistics_from_database function is now defined at the top of the impl block } /// Represents the data submitted in the proposal form -- 2.40.1 From b60692310202b24388fe3f43fc590d2c04ba1f0d Mon Sep 17 00:00:00 2001 From: Mahmoud-Emad Date: Wed, 21 May 2025 15:56:47 +0300 Subject: [PATCH 08/26] feat: Finish the proposal dashboard --- actix_mvc_app/src/views/governance/index.html | 301 +++++++++--------- 1 file changed, 156 insertions(+), 145 deletions(-) diff --git a/actix_mvc_app/src/views/governance/index.html b/actix_mvc_app/src/views/governance/index.html index 6ca829e..6765880 100644 --- a/actix_mvc_app/src/views/governance/index.html +++ b/actix_mvc_app/src/views/governance/index.html @@ -3,170 +3,181 @@ {% block title %}Governance Dashboard{% endblock %} {% block content %} - -
-
- + +
+
+ +
+
+ + +
+
+
+ +
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.

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

+
+ +
+
65% Yes
+
35% No
+
+ +
+ 26 votes cast + Quorum: 75% reached +
+ +
+
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.timestamp | 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 -- 2.40.1 From 67b80f237dce0e878271e91e8fb632b420bb66eb Mon Sep 17 00:00:00 2001 From: Mahmoud-Emad Date: Wed, 21 May 2025 18:01:22 +0300 Subject: [PATCH 09/26] feat: Enahnced the dashboard --- actix_mvc_app/src/controllers/governance.rs | 5 +++ actix_mvc_app/src/views/governance/index.html | 36 +++++++++++++++---- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/actix_mvc_app/src/controllers/governance.rs b/actix_mvc_app/src/controllers/governance.rs index 073b8e1..04d4b89 100644 --- a/actix_mvc_app/src/controllers/governance.rs +++ b/actix_mvc_app/src/controllers/governance.rs @@ -112,7 +112,12 @@ impl GovernanceController { // 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); } // Calculate statistics from the database diff --git a/actix_mvc_app/src/views/governance/index.html b/actix_mvc_app/src/views/governance/index.html index 6765880..ec7351e 100644 --- a/actix_mvc_app/src/views/governance/index.html +++ b/actix_mvc_app/src/views/governance/index.html @@ -63,16 +63,40 @@

{{ 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 %} +
-
65% Yes
-
35% No
+
{{ yes_percent }}% Yes +
+
{{ no_percent }}% No +
+
{{ abstain_percent + }}% Abstain +
- 26 votes cast - Quorum: 75% reached + {{ total_votes }} votes cast + Quorum: {% if total_votes >= 20 %}75% reached{% else %}Not reached{% endif %}
-- 2.40.1 From 4659697ae22729ede255f4a05a495812bb6f8d56 Mon Sep 17 00:00:00 2001 From: Mahmoud-Emad Date: Thu, 22 May 2025 15:47:11 +0300 Subject: [PATCH 10/26] feat: Add filtering and searching to governance proposals page - Added filtering of proposals by status (Draft, Active, Approved, Rejected, Cancelled). - Added searching of proposals by title and description. - Improved UI to persist filter and search values. - Added a "No proposals found" message for better UX. --- actix_mvc_app/src/controllers/governance.rs | 48 ++++++++++++++++++- .../src/views/governance/proposals.html | 27 ++++++++--- 2 files changed, 66 insertions(+), 9 deletions(-) diff --git a/actix_mvc_app/src/controllers/governance.rs b/actix_mvc_app/src/controllers/governance.rs index 04d4b89..71e90df 100644 --- a/actix_mvc_app/src/controllers/governance.rs +++ b/actix_mvc_app/src/controllers/governance.rs @@ -133,7 +133,11 @@ impl GovernanceController { } /// 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"); @@ -144,14 +148,47 @@ impl GovernanceController { } // Get proposals from the database - let proposals = match crate::db::proposals::get_proposals() { + let mut proposals = match crate::db::proposals::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) } @@ -597,6 +634,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/views/governance/proposals.html b/actix_mvc_app/src/views/governance/proposals.html index dba23f2..e0c565e 100644 --- a/actix_mvc_app/src/views/governance/proposals.html +++ b/actix_mvc_app/src/views/governance/proposals.html @@ -58,18 +58,18 @@
+ placeholder="Search by title or description" value="{% if search_filter %}{{ search_filter }}{% endif %}">
@@ -89,6 +89,7 @@ Create New Proposal
+ {% if proposals and proposals|length > 0 %}
@@ -129,9 +130,21 @@ {% endfor %}
+ {% else %} +
+ +
No proposals found
+ {% if status_filter or search_filter %} +

No proposals match your current filter criteria. Try adjusting your filters or view all proposals.

+ {% else %} +

There are no proposals in the system yet.

+ {% endif %} + Create New Proposal
+ {% endif %}
+
{% endblock %} \ No newline at end of file -- 2.40.1 From fad288f67da1eccca17398ebc0d662ac645acd25 Mon Sep 17 00:00:00 2001 From: Mahmoud-Emad Date: Thu, 22 May 2025 16:08:12 +0300 Subject: [PATCH 11/26] feat: Add total vote counts to governance views - Add functionality to calculate total yes, no, and abstain votes across all proposals. This provides a summary of community voting patterns on the governance page. - Improve the user experience by displaying total vote counts prominently on the "My Votes" page. This gives users a quick overview of the overall voting results. - Enhance the "Create Proposal" page with informative guidelines and a helpful alert to guide users through the proposal creation process. This improves clarity and ensures proposals are well- structured. --- actix_mvc_app/src/controllers/governance.rs | 30 +++++ .../src/views/governance/create_proposal.html | 42 ++++--- .../src/views/governance/my_votes.html | 105 +++++++++--------- 3 files changed, 106 insertions(+), 71 deletions(-) diff --git a/actix_mvc_app/src/controllers/governance.rs b/actix_mvc_app/src/controllers/governance.rs index 71e90df..aa51af5 100644 --- a/actix_mvc_app/src/controllers/governance.rs +++ b/actix_mvc_app/src/controllers/governance.rs @@ -443,6 +443,12 @@ impl GovernanceController { } } + // 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) @@ -607,6 +613,30 @@ impl GovernanceController { } // 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) + } } /// Represents the data submitted in the proposal form diff --git a/actix_mvc_app/src/views/governance/create_proposal.html b/actix_mvc_app/src/views/governance/create_proposal.html index eb91862..c81435d 100644 --- a/actix_mvc_app/src/views/governance/create_proposal.html +++ b/actix_mvc_app/src/views/governance/create_proposal.html @@ -4,13 +4,6 @@ {% block content %}
-
-
-

Create Governance Proposal

-

Submit a new proposal for the community to vote on.

-
-
-
@@ -30,11 +23,26 @@
- - + + +
+
+
+ +
About Creating Proposals
+

Creating a proposal is an important step in our community governance process. Well-crafted proposals clearly state the problem, solution, and implementation details. The community will review and vote on your proposal, so be thorough and thoughtful in your submission.

+ +
+
+
+ +
-
-
+ +
+
New Proposal
@@ -49,7 +57,7 @@
-
Explain the purpose, benefits, and implementation details
@@ -84,12 +92,10 @@
-
- - -
-
-
+ + +
+
Proposal Guidelines
diff --git a/actix_mvc_app/src/views/governance/my_votes.html b/actix_mvc_app/src/views/governance/my_votes.html index 9a1085d..bca1cae 100644 --- a/actix_mvc_app/src/views/governance/my_votes.html +++ b/actix_mvc_app/src/views/governance/my_votes.html @@ -23,6 +23,57 @@
+ +
+
+
+ +
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.

+ +
+
+
+ + +
+
+
+
+
Yes Votes
+

+ {{ total_yes_votes }} +

+
+
+
+
+
+
+
No Votes
+

+ {{ total_no_votes }} +

+
+
+
+
+
+
+
Abstain Votes
+

+ {{ total_abstain_votes }} +

+
+
+
+
+
@@ -82,57 +133,5 @@
- -{% 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 }} -

-
-
-
-
-{% endif %} + {% endblock %} \ No newline at end of file -- 2.40.1 From 52fbc77e3ef5f24705cf78a2c603007e7f68c7b6 Mon Sep 17 00:00:00 2001 From: Mahmoud-Emad Date: Thu, 22 May 2025 16:31:11 +0300 Subject: [PATCH 12/26] feat: Enhance proposal creation and display - Improve proposal creation form with input validation and default date settings for a better user experience. - Add context variables to the proposals template for consistent display across governance pages. - Enhance proposal detail page with visual improvements, voting results display, and user voting functionality. - Add styles for better visual presentation of proposal details and voting information. --- actix_mvc_app/src/controllers/governance.rs | 5 + .../src/views/governance/create_proposal.html | 144 +++++- .../src/views/governance/proposal_detail.html | 459 ++++++++++++------ 3 files changed, 459 insertions(+), 149 deletions(-) diff --git a/actix_mvc_app/src/controllers/governance.rs b/actix_mvc_app/src/controllers/governance.rs index aa51af5..f0795e9 100644 --- a/actix_mvc_app/src/controllers/governance.rs +++ b/actix_mvc_app/src/controllers/governance.rs @@ -340,6 +340,11 @@ impl GovernanceController { } }; 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::); render_template(&tmpl, "governance/proposals.html", &ctx) } diff --git a/actix_mvc_app/src/views/governance/create_proposal.html b/actix_mvc_app/src/views/governance/create_proposal.html index c81435d..6636e92 100644 --- a/actix_mvc_app/src/views/governance/create_proposal.html +++ b/actix_mvc_app/src/views/governance/create_proposal.html @@ -23,21 +23,24 @@
- +
About Creating Proposals
-

Creating a proposal is an important step in our community governance process. Well-crafted proposals clearly state the problem, solution, and implementation details. The community will review and vote on your proposal, so be thorough and thoughtful in your submission.

+

Creating a proposal is an important step in our community governance process. Well-crafted proposals + clearly state the problem, solution, and implementation details. The community will review and vote + on your proposal, so be thorough and thoughtful in your submission.

- +
@@ -47,34 +50,42 @@
New Proposal
-
+
- + +
Please provide a title (5-100 characters).
Make it descriptive and specific
- +
+ minlength="50" maxlength="5000" + placeholder="Provide a detailed description of your proposal..."> +
Please provide a detailed description (at least 50 + characters).
Explain the purpose, benefits, and implementation details
- +
+
Please select a valid start date. +
When should voting begin?
+
End date must be after start date. +
When should voting end?
- +
@@ -83,7 +94,7 @@
- +
Cancel @@ -92,7 +103,7 @@
- +
@@ -122,4 +133,111 @@
+ +{% block scripts %} + {% endblock %} + +{% 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 a4fba74..f7c3303 100644 --- a/actix_mvc_app/src/views/governance/proposal_detail.html +++ b/actix_mvc_app/src/views/governance/proposal_detail.html @@ -2,6 +2,37 @@ {% block title %}{{ proposal.title }} - Governance Proposal{% endblock %} +{% block styles %} + +{% endblock %} + {% block content %}
@@ -30,44 +61,62 @@
-
-
-
+
+
+

{{ proposal.title }}

-
-
+
+
+ {{ proposal.status }} - Created by {{ proposal.creator_name }} - + Created by {{ proposal.creator_name + }}
-
Description
-

{{ proposal.description }}

+
+
Description
+
{{ proposal.description }}
+
-
Voting Period
-

- {% if proposal.voting_starts_at and proposal.voting_ends_at %} - Start: {{ proposal.voting_starts_at | date(format="%Y-%m-%d") }}
- End: {{ proposal.voting_ends_at | date(format="%Y-%m-%d") }} - {% else %} - Not set - {% endif %} -

+
+
Voting Period
+
+ {% if proposal.voting_starts_at and proposal.voting_ends_at %} +
+
Start Date
+
{{ proposal.voting_starts_at | date(format="%Y-%m-%d") }}
+
+
+ +
+
+
End Date
+
{{ proposal.voting_ends_at | date(format="%Y-%m-%d") }}
+
+ {% else %} +
Not set
+ {% endif %} +
+
-
-
-
-
Voting Results
+
+
+
+
Voting Dashboard
-
-
+
+ +
+
Results
+ {% set yes_percent = 0 %} {% set no_percent = 0 %} {% set abstain_percent = 0 %} @@ -78,131 +127,269 @@ {% set abstain_percent = (results.abstain_count * 100 / results.total_votes) | int %} {% endif %} -

Yes: {{ results.yes_count }} ({{ yes_percent }}%)

-
-
-
+ +
+ Yes + {{ results.yes_count }} +
+
+
-

No: {{ results.no_count }} ({{ no_percent }}%)

-
-
-
+ +
+ No + {{ results.no_count }} +
+
+
-

Abstain: {{ results.abstain_count }} ({{ abstain_percent }}%)

-
+ +
+ + Abstain + {{ results.abstain_count }} +
+
+ style="width: {{ abstain_percent }}%" aria-valuenow="{{ abstain_percent }}" + aria-valuemin="0" aria-valuemax="100" title="{{ abstain_percent }}% of votes">
-

Total Votes: {{ results.total_votes }}

-
-
- - {% if proposal.status == "Active" and user and user.id %} -
-
-
Cast Your Vote
-
-
- -
- -
- - +
+
+
+

{{ results.total_votes }}

+ Total Votes
-
- - -
-
- - -
-
-
- - -
- - -
-
- {% elif not user or not user.id %} -
-
-

You must be logged in to vote.

- Login to Vote -
-
- {% elif proposal.status != "Active" %} -
-
-
- - Note: Voting is only available for proposals with an Active status. - This proposal's current status is {{ proposal.status }}. -
-
-
- {% endif %} -
-
- -
-
-
-
-
Votes ({{ votes | length }})
-
-
- {% if votes | length > 0 %} -
- - - - - - - - - - - {% for vote in votes %} - - - - - - - {% endfor %} - -
VoterVoteCommentDate
{{ vote.voter_name }} - - {{ vote.vote_type }} - - {% if vote.comment %}{{ vote.comment }}{% else %}No comment{% endif %}{{ vote.created_at | date(format="%Y-%m-%d %H:%M") }}
+ {% if proposal.status == "Active" %} +
+
+ + + + + +
+ {{ yes_percent }}%
+
+ Approval Rate +
+ {% endif %} +
+
+ + + {% if proposal.status == "Active" and user and user.id %} +
+
Cast Your Vote
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ +
+
+ {% elif proposal.status != "Active" %} +
+ +

Voting is {{ proposal.status | lower }} for this proposal

+
+ {% elif not user or not user.id %} +
+ +

You must be logged in to vote

+ Login to Vote
- {% else %} -

No votes have been cast yet.

{% endif %}
+ + +
+
+
+
+
Votes
+
+
+ + + + +
+
+ + + + +
+
+
+
+
+ + + + + + + + + + + {% if votes | length == 0 %} + + + + {% else %} + {% for vote in votes %} + + + + + + + {% endfor %} + {% endif %} + +
VoterVoteCommentDate
+
+ +

No votes have been cast yet

+
+
+
+
+ U +
+ {{ vote.voter_name }} +
+
+ + {% if vote.vote_type == 'Yes' %} + + {% elif vote.vote_type == 'No' %} + + {% else %} + + {% endif %} + {{ vote.vote_type }} + + + {% if vote.comment %} +
{{ vote.comment }}
+ {% else %} + No comment provided + {% endif %} +
+
+ {{ vote.created_at | date(format="%Y-%m-%d") }} + {{ vote.created_at | date(format="%H:%M") + }} +
+
+
+
+
+
+
-
-{% endblock %} \ No newline at end of file + + {% block scripts %} + + {% endblock scripts %} + + {% endblock content %} \ No newline at end of file -- 2.40.1 From 3d8aca19cc4623906f288a08d24e118dbe5eea72 Mon Sep 17 00:00:00 2001 From: Mahmoud-Emad Date: Thu, 22 May 2025 17:05:26 +0300 Subject: [PATCH 13/26] feat: Improve user experience after voting on proposals - Redirect users to the proposal detail page with a success message after a successful vote, improving feedback. - Automatically remove the success message from the URL after a short time to avoid URL clutter and maintain a clean browsing experience. - Add a success alert message on the proposal detail page to provide immediate visual confirmation of a successful vote. - Improve the visual presentation of the votes list on the proposal detail page by adding top margin for better spacing. --- actix_mvc_app/src/controllers/governance.rs | 25 ++++++++++--------- .../src/views/governance/proposal_detail.html | 19 +++++++++++++- 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/actix_mvc_app/src/controllers/governance.rs b/actix_mvc_app/src/controllers/governance.rs index f0795e9..0a4d032 100644 --- a/actix_mvc_app/src/controllers/governance.rs +++ b/actix_mvc_app/src/controllers/governance.rs @@ -196,9 +196,13 @@ impl GovernanceController { /// Handles the proposal detail page route pub async fn proposal_detail( path: web::Path, + 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"); @@ -220,6 +224,11 @@ impl GovernanceController { // 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 { @@ -392,18 +401,10 @@ impl GovernanceController { form.comment.as_ref().map(|s| s.to_string()), // Pass the comment from the form ) { Ok(updated_proposal) => { - ctx.insert("proposal", &updated_proposal); - ctx.insert("success", "Your vote has been recorded!"); - - // Extract votes directly from the updated proposal - let votes = Self::extract_votes_from_proposal(&updated_proposal); - ctx.insert("votes", &votes); - - // Calculate voting results directly from the updated proposal - let results = Self::calculate_voting_results_from_proposal(&updated_proposal); - ctx.insert("results", &results); - - render_template(&tmpl, "governance/proposal_detail.html", &ctx) + // 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)); diff --git a/actix_mvc_app/src/views/governance/proposal_detail.html b/actix_mvc_app/src/views/governance/proposal_detail.html index f7c3303..e6f8323 100644 --- a/actix_mvc_app/src/views/governance/proposal_detail.html +++ b/actix_mvc_app/src/views/governance/proposal_detail.html @@ -240,7 +240,7 @@
-
+
@@ -338,6 +338,23 @@ {% block scripts %} - {% endblock scripts %} - {% endblock content %} \ No newline at end of file + // Hide all rows first + voteRows.forEach(row => row.style.display = 'none'); + + // Calculate pagination + const totalRows = filteredRows.length; + const totalPages = Math.max(1, Math.ceil(totalRows / rowsPerPage)); + + // Ensure current page is valid + if (currentPage > totalPages) { + currentPage = totalPages; + } + + // Show only rows for current page + const start = (currentPage - 1) * rowsPerPage; + const end = start + rowsPerPage; + + filteredRows.slice(start, end).forEach(row => row.style.display = ''); + + // Update pagination info + if (startRowElement && endRowElement && totalRowsElement) { + startRowElement.textContent = totalRows > 0 ? start + 1 : 0; + endRowElement.textContent = Math.min(end, totalRows); + totalRowsElement.textContent = totalRows; + } + } + + // Event listeners for pagination + if (prevPageBtn) { + prevPageBtn.addEventListener('click', function (e) { + e.preventDefault(); + if (currentPage > 1) { + currentPage--; + updatePagination(); + } + }); + } + + if (nextPageBtn) { + nextPageBtn.addEventListener('click', function (e) { + e.preventDefault(); + // Get all rows that match the current filter + const currentFilter = document.querySelector('[data-filter].active'); + const filterType = currentFilter ? currentFilter.getAttribute('data-filter') : 'all'; + + // Get rows that match the current filter and search term + let filteredRows = Array.from(voteRows); + if (filterType !== 'all') { + filteredRows = filteredRows.filter(row => row.getAttribute('data-vote-type') === filterType); + } + + // Apply search filter if there's a search term + const searchTerm = searchInput ? searchInput.value.toLowerCase() : ''; + if (searchTerm) { + filteredRows = filteredRows.filter(row => { + const voterName = row.querySelector('td:first-child').textContent.toLowerCase(); + const comment = row.querySelector('td:nth-child(3)').textContent.toLowerCase(); + return voterName.includes(searchTerm) || comment.includes(searchTerm); + }); + } + + const totalRows = filteredRows.length; + const totalPages = Math.max(1, Math.ceil(totalRows / rowsPerPage)); + + if (currentPage < totalPages) { + currentPage++; + updatePagination(); + } + }); + } + + if (rowsPerPageSelect) { + rowsPerPageSelect.addEventListener('change', function () { + rowsPerPage = parseInt(this.value); + currentPage = 1; // Reset to first page + updatePagination(); + }); + } + + // Initialize pagination (but don't interfere with filtering) + if (paginationControls) { + // Only initialize pagination if there are many votes + // The filtering will handle showing/hiding rows + console.log('Pagination controls available but not interfering with filtering'); + } + + // Initialize tooltips for all elements with title attributes + const tooltipElements = document.querySelectorAll('[title]'); + if (tooltipElements.length > 0) { + [].slice.call(tooltipElements).map(function (el) { + return new bootstrap.Tooltip(el); + }); + } + + // Add debugging for vote form + const voteForm = document.getElementById('voteForm'); + if (voteForm) { + console.log('Vote form found:', voteForm); + voteForm.addEventListener('submit', function (e) { + console.log('Vote form submitted'); + const formData = new FormData(voteForm); + console.log('Form data:', Object.fromEntries(formData)); + }); + } else { + console.log('Vote form not found'); + } + + // Debug logging + console.log('Filter buttons found:', filterButtons.length); + console.log('Vote rows found:', voteRows.length); + console.log('Search input found:', searchInput ? 'Yes' : 'No'); + }); + +{% endblock %} \ No newline at end of file diff --git a/actix_mvc_app/src/views/governance/proposals.html b/actix_mvc_app/src/views/governance/proposals.html index e0c565e..fd23e97 100644 --- a/actix_mvc_app/src/views/governance/proposals.html +++ b/actix_mvc_app/src/views/governance/proposals.html @@ -3,6 +3,12 @@ {% block title %}Proposals - Governance Dashboard{% endblock %} {% block content %} + +{% include "governance/_header.html" %} + + +{% include "governance/_tabs.html" %} + {% if success %}
@@ -15,26 +21,6 @@
{% endif %} - -
-
- -
-
-
@@ -58,18 +44,23 @@
+ placeholder="Search by title or description" + value="{% if search_filter %}{{ search_filter }}{% endif %}">
@@ -130,21 +121,22 @@ {% endfor %} - {% else %} -
- -
No proposals found
- {% if status_filter or search_filter %} -

No proposals match your current filter criteria. Try adjusting your filters or view all proposals.

{% else %} -

There are no proposals in the system yet.

+
+ +
No proposals found
+ {% if status_filter or search_filter %} +

No proposals match your current filter criteria. Try adjusting your filters or view all proposals.

+ {% else %} +

There are no proposals in the system yet.

+ {% endif %} + Create New Proposal +
{% endif %} - Create New Proposal
- {% endif %}
-
{% endblock %} \ No newline at end of file -- 2.40.1 From 11d7ae37b68ed6e4ebf071ea819cb93af6057254 Mon Sep 17 00:00:00 2001 From: Mahmoud-Emad Date: Tue, 27 May 2025 20:45:30 +0300 Subject: [PATCH 17/26] feat: Enhance governance module with activity tracking and DB refactor - Refactor database interaction for proposals and activities. - Add activity tracking for proposal creation and voting. - Improve logging for better debugging and monitoring. - Update governance views to display recent activities. - Add strum and strum_macros crates for enum handling. - Update Cargo.lock file with new dependencies. --- actix_mvc_app/Cargo.lock | 29 +++- actix_mvc_app/src/controllers/governance.rs | 93 ++++++++---- actix_mvc_app/src/db/db.rs | 17 +++ actix_mvc_app/src/db/governance_tracker.rs | 139 ------------------ actix_mvc_app/src/db/mod.rs | 2 +- actix_mvc_app/src/db/proposals.rs | 100 +++++++++---- actix_mvc_app/src/models/governance.rs | 136 +---------------- .../src/views/governance/proposal_detail.html | 6 +- .../src/views/governance/proposals.html | 6 +- 9 files changed, 193 insertions(+), 335 deletions(-) create mode 100644 actix_mvc_app/src/db/db.rs delete mode 100644 actix_mvc_app/src/db/governance_tracker.rs diff --git a/actix_mvc_app/Cargo.lock b/actix_mvc_app/Cargo.lock index 63aeda6..ee0c02c 100644 --- a/actix_mvc_app/Cargo.lock +++ b/actix_mvc_app/Cargo.lock @@ -1329,6 +1329,12 @@ 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" @@ -1351,6 +1357,8 @@ dependencies = [ "rhai_wrapper", "serde", "serde_json", + "strum", + "strum_macros", "tst", ] @@ -2344,7 +2352,7 @@ dependencies = [ name = "rhai_autobind_macros" version = "0.1.0" dependencies = [ - "heck", + "heck 0.4.1", "proc-macro2", "quote", "syn", @@ -2701,6 +2709,25 @@ 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" diff --git a/actix_mvc_app/src/controllers/governance.rs b/actix_mvc_app/src/controllers/governance.rs index 6536efd..a8ca20b 100644 --- a/actix_mvc_app/src/controllers/governance.rs +++ b/actix_mvc_app/src/controllers/governance.rs @@ -1,10 +1,13 @@ -use crate::db::governance_tracker; -use crate::db::proposals::{self, get_proposal_by_id}; +use crate::db::proposals::{ + self, create_activity, get_all_activities, get_proposal_by_id, get_proposals, + get_recent_activities, +}; use crate::models::governance::{Vote, VoteType, VotingResults}; 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; @@ -80,6 +83,7 @@ impl GovernanceController { /// Handles the governance dashboard page route pub async fn index(tmpl: web::Data, session: Session) -> Result { + println!("=============================================="); let mut ctx = tera::Context::new(); ctx.insert("active_page", "governance"); ctx.insert("active_tab", "dashboard"); @@ -96,14 +100,32 @@ impl GovernanceController { let user = Self::get_user_from_session(&session).unwrap(); ctx.insert("user", &user); + println!("=============================================="); // Get proposals from the database let proposals = match crate::db::proposals::get_proposals() { - Ok(props) => props, + 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![] } }; + println!("=============================================="); // Make a copy of proposals for statistics let proposals_for_stats = proposals.clone(); @@ -170,8 +192,9 @@ impl GovernanceController { ctx.insert("user", &user); } + println!("============== Loading proposals ================="); // Get proposals from the database - let mut proposals = match crate::db::proposals::get_proposals() { + let mut proposals = match get_proposals() { Ok(props) => props, Err(e) => { ctx.insert("error", &format!("Failed to load proposals: {}", e)); @@ -179,6 +202,8 @@ impl GovernanceController { } }; + println!("proposals: {:?}", proposals); + // Filter proposals by status if provided if let Some(status_filter) = &query.status { if !status_filter.is_empty() { @@ -372,15 +397,12 @@ impl GovernanceController { ); // Track the proposal creation activity - let creation_activity = - crate::models::governance::GovernanceActivity::proposal_created( - proposal_id, - &saved_proposal.title, - &user_id, - &user_name, - ); - - let _ = governance_tracker::create_activity(creation_activity); + let _ = create_activity( + proposal_id, + &saved_proposal.title, + &user_name, + &ActivityType::ProposalCreated, + ); ctx.insert("success", "Proposal created successfully!"); } @@ -394,8 +416,24 @@ impl GovernanceController { // Get proposals from the database let proposals = match crate::db::proposals::get_proposals() { - Ok(props) => props, + 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![] } @@ -407,6 +445,14 @@ impl GovernanceController { 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) } @@ -461,15 +507,12 @@ impl GovernanceController { // Track the vote cast activity if let Ok(Some(proposal)) = get_proposal_by_id(proposal_id_u32) { - let vote_activity = crate::models::governance::GovernanceActivity::vote_cast( + let _ = create_activity( proposal_id_u32, &proposal.title, user_name, - &form.vote_type, - 1, // shares + &ActivityType::VoteCast, ); - - let _ = governance_tracker::create_activity(vote_activity); } // Redirect to the proposal detail page with a success message @@ -576,7 +619,7 @@ impl GovernanceController { /// 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 = governance_tracker::get_recent_activities()?; + let activities = get_recent_activities()?; // Convert GovernanceActivity to the format expected by the template let formatted_activities: Vec = activities @@ -596,10 +639,10 @@ impl GovernanceController { serde_json::json!({ "type": activity.activity_type, "icon": icon, - "user": activity.actor_name, + "user": activity.creator_name, "action": action, "proposal_title": activity.proposal_title, - "timestamp": activity.timestamp.format("%Y-%m-%dT%H:%M:%SZ").to_string(), + "created_at": activity.created_at.format("%Y-%m-%dT%H:%M:%SZ").to_string(), "proposal_id": activity.proposal_id }) }) @@ -611,7 +654,7 @@ impl GovernanceController { /// Get all governance activities from the database fn get_all_governance_activities() -> Result, String> { // Get all activities from the database - let activities = governance_tracker::get_all_activities()?; + let activities = get_all_activities()?; // Convert GovernanceActivity to the format expected by the template let formatted_activities: Vec = activities @@ -631,10 +674,10 @@ impl GovernanceController { serde_json::json!({ "type": activity.activity_type, "icon": icon, - "user": activity.actor_name, + "user": activity.creator_name, "action": action, "proposal_title": activity.proposal_title, - "timestamp": activity.timestamp.format("%Y-%m-%dT%H:%M:%SZ").to_string(), + "created_at": activity.created_at.format("%Y-%m-%dT%H:%M:%SZ").to_string(), "proposal_id": activity.proposal_id }) }) 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_tracker.rs b/actix_mvc_app/src/db/governance_tracker.rs deleted file mode 100644 index 5ade2ae..0000000 --- a/actix_mvc_app/src/db/governance_tracker.rs +++ /dev/null @@ -1,139 +0,0 @@ -use crate::models::governance::GovernanceActivity; -use std::path::PathBuf; - -/// Database path for governance activities -pub const DB_PATH: &str = "/tmp/ourdb_governance_activities"; - -/// Returns a shared OurDB instance for activities -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); - } - let db = heromodels::db::hero::OurDB::new(db_path, true) - .map_err(|e| format!("Failed to create activities DB: {:?}", e))?; - Ok(db) -} - -/// Creates a new governance activity and saves it to the database using OurDB -pub fn create_activity(activity: GovernanceActivity) -> Result<(u32, GovernanceActivity), String> { - let db = get_db()?; - - // Since OurDB doesn't support custom models directly, we'll use a simple key-value approach - // Store each activity with a unique key and serialize it as JSON - - // First, get the next available ID by checking existing keys - let activity_id = get_next_activity_id(&db)?; - - // Create the activity with the assigned ID - let mut new_activity = activity; - new_activity.id = Some(activity_id); - - // Serialize the activity to JSON - let activity_json = serde_json::to_string(&new_activity) - .map_err(|e| format!("Failed to serialize activity: {}", e))?; - - // Store in OurDB using a key-value approach - let key = format!("activity_{}", activity_id); - - // Use OurDB's raw storage capabilities to store the JSON string - // Since we can't use collections directly, we'll store as raw data - let db_path = format!("{}/{}.json", DB_PATH, key); - std::fs::write(&db_path, &activity_json) - .map_err(|e| format!("Failed to write activity to DB: {}", e))?; - - // Also maintain an index of activity IDs for efficient retrieval - update_activity_index(&db, activity_id)?; - - println!( - "✅ Activity recorded: {} - {}", - new_activity.activity_type, new_activity.description - ); - - Ok((activity_id, new_activity)) -} - -/// Gets the next available activity ID -fn get_next_activity_id(_db: &heromodels::db::hero::OurDB) -> Result { - let index_path = format!("{}/activity_index.json", DB_PATH); - - if std::path::Path::new(&index_path).exists() { - let content = std::fs::read_to_string(&index_path) - .map_err(|e| format!("Failed to read activity index: {}", e))?; - let index: Vec = serde_json::from_str(&content).unwrap_or_else(|_| Vec::new()); - Ok(index.len() as u32 + 1) - } else { - Ok(1) - } -} - -/// Updates the activity index with a new activity ID -fn update_activity_index( - _db: &heromodels::db::hero::OurDB, - activity_id: u32, -) -> Result<(), String> { - let index_path = format!("{}/activity_index.json", DB_PATH); - - let mut index: Vec = if std::path::Path::new(&index_path).exists() { - let content = std::fs::read_to_string(&index_path) - .map_err(|e| format!("Failed to read activity index: {}", e))?; - serde_json::from_str(&content).unwrap_or_else(|_| Vec::new()) - } else { - Vec::new() - }; - - index.push(activity_id); - - let content = serde_json::to_string(&index) - .map_err(|e| format!("Failed to serialize activity index: {}", e))?; - - std::fs::write(&index_path, content) - .map_err(|e| format!("Failed to write activity index: {}", e))?; - - Ok(()) -} - -/// Gets all activities from the database using OurDB -pub fn get_all_activities() -> Result, String> { - let _db = get_db()?; - let index_path = format!("{}/activity_index.json", DB_PATH); - - // Read the activity index to get all activity IDs - if !std::path::Path::new(&index_path).exists() { - return Ok(Vec::new()); - } - - let content = std::fs::read_to_string(&index_path) - .map_err(|e| format!("Failed to read activity index: {}", e))?; - let activity_ids: Vec = serde_json::from_str(&content).unwrap_or_else(|_| Vec::new()); - - let mut activities = Vec::new(); - - // Load each activity by ID - for activity_id in activity_ids { - let activity_path = format!("{}/activity_{}.json", DB_PATH, activity_id); - if std::path::Path::new(&activity_path).exists() { - let activity_content = std::fs::read_to_string(&activity_path) - .map_err(|e| format!("Failed to read activity {}: {}", activity_id, e))?; - - if let Ok(activity) = serde_json::from_str::(&activity_content) { - activities.push(activity); - } - } - } - - Ok(activities) -} - -/// Gets recent activities (last 10) sorted by timestamp using OurDB -pub fn get_recent_activities() -> Result, String> { - let mut activities = get_all_activities()?; - - // Sort by timestamp (most recent first) - activities.sort_by(|a, b| b.timestamp.cmp(&a.timestamp)); - - // Take only the last 10 - activities.truncate(10); - - Ok(activities) -} diff --git a/actix_mvc_app/src/db/mod.rs b/actix_mvc_app/src/db/mod.rs index 1b1e306..6def13b 100644 --- a/actix_mvc_app/src/db/mod.rs +++ b/actix_mvc_app/src/db/mod.rs @@ -1,2 +1,2 @@ -pub mod governance_tracker; +pub mod db; pub mod proposals; diff --git a/actix_mvc_app/src/db/proposals.rs b/actix_mvc_app/src/db/proposals.rs index c12faa9..e354c90 100644 --- a/actix_mvc_app/src/db/proposals.rs +++ b/actix_mvc_app/src/db/proposals.rs @@ -1,25 +1,10 @@ -use std::path::PathBuf; - use chrono::{Duration, Utc}; -use heromodels::db::hero::OurDB; use heromodels::{ db::{Collection, Db}, - models::governance::{Proposal, ProposalStatus}, + models::governance::{Activity, ActivityType, Proposal, ProposalStatus}, }; -/// The path to the database file. Change this as needed for your environment. -pub const DB_PATH: &str = "/tmp/ourdb_governance"; - -/// 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(db_path: &str) -> 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) -} +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( @@ -31,7 +16,7 @@ pub fn create_new_proposal( voting_start_date: Option>, voting_end_date: Option>, ) -> Result<(u32, Proposal), String> { - let db = get_db(DB_PATH).expect("Can create DB"); + let db = get_db().expect("Can get DB"); let created_at = Utc::now(); let updated_at = created_at; @@ -60,7 +45,7 @@ pub fn create_new_proposal( /// Loads all proposals from the database and returns them as a Vec. pub fn get_proposals() -> Result, String> { - let db = get_db(DB_PATH).map_err(|e| format!("DB error: {}", e))?; + let db = get_db().map_err(|e| format!("DB error: {}", e))?; let collection = db .collection::() .expect("can open proposal collection"); @@ -78,7 +63,7 @@ pub fn get_proposals() -> Result, String> { /// 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(DB_PATH).map_err(|e| format!("DB error: {}", e))?; + let db = get_db().map_err(|e| format!("DB error: {}", e))?; let collection = db .collection::() .map_err(|e| format!("Collection error: {:?}", e))?; @@ -100,7 +85,7 @@ pub fn submit_vote_on_proposal( comment: Option, ) -> Result { // Get the proposal from the database - let db = get_db(DB_PATH).map_err(|e| format!("DB error: {}", e))?; + let db = get_db().map_err(|e| format!("DB error: {}", e))?; let collection = db .collection::() .map_err(|e| format!("Collection error: {:?}", e))?; @@ -167,13 +152,6 @@ pub fn submit_vote_on_proposal( // We'll create a simple ballot with an auto-generated ID let ballot_id = proposal.ballots.len() as u32 + 1; - // We need to manually create a ballot since we can't use cast_vote - // This is a simplified version that just records the vote - println!( - "Recording vote: ballot_id={}, user_id={}, option_id={}, shares={}", - ballot_id, user_id, option_id, shares_count - ); - // Create a new ballot and add it to the proposal's ballots use heromodels::models::governance::Ballot; @@ -210,3 +188,69 @@ pub fn submit_vote_on_proposal( Ok(updated_proposal) } + +/// 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/models/governance.rs b/actix_mvc_app/src/models/governance.rs index 34f05b2..b2e2eef 100644 --- a/actix_mvc_app/src/models/governance.rs +++ b/actix_mvc_app/src/models/governance.rs @@ -208,108 +208,8 @@ pub struct VotingResults { pub total_votes: usize, } -/// Represents a governance activity in the system -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct GovernanceActivity { - /// Unique identifier for the activity - pub id: Option, - /// Type of activity (proposal_created, vote_cast, etc.) - pub activity_type: String, - /// ID of the related proposal - pub proposal_id: u32, - /// Title of the related proposal - pub proposal_title: String, - /// Name of the user who performed the action - pub actor_name: String, - /// Description of the activity - pub description: String, - /// Date and time when the activity occurred - pub timestamp: DateTime, -} - -impl GovernanceActivity { - /// Creates a new governance activity - pub fn new( - activity_type: &str, - proposal_id: u32, - proposal_title: &str, - actor_name: &str, - description: &str, - ) -> Self { - Self { - id: None, - activity_type: activity_type.to_string(), - proposal_id, - proposal_title: proposal_title.to_string(), - actor_name: actor_name.to_string(), - description: description.to_string(), - timestamp: Utc::now(), - } - } - - /// Creates a proposal creation activity - pub fn proposal_created( - proposal_id: u32, - proposal_title: &str, - _creator_id: &str, - creator_name: &str, - ) -> Self { - Self::new( - "proposal_created", - proposal_id, - proposal_title, - creator_name, - &format!("Proposal '{}' created by {}", proposal_title, creator_name), - ) - } - - /// Creates a vote cast activity - pub fn vote_cast( - proposal_id: u32, - proposal_title: &str, - voter_name: &str, - vote_option: &str, - shares: i64, - ) -> Self { - Self::new( - "vote_cast", - proposal_id, - proposal_title, - voter_name, - &format!( - "{} voted '{}' with {} shares", - voter_name, vote_option, shares - ), - ) - } - - /// Creates a proposal status change activity - pub fn proposal_status_changed( - proposal_id: u32, - proposal_title: &str, - new_status: &ProposalStatus, - reason: Option<&str>, - ) -> Self { - let description = format!( - "Proposal '{}' status changed to {}{}", - proposal_title, - new_status, - reason.map(|r| format!(": {}", r)).unwrap_or_default() - ); - - Self::new( - "proposal_status_changed", - proposal_id, - proposal_title, - "System", - &description, - ) - } -} - -#[allow(dead_code)] impl VotingResults { - /// Creates a new empty voting results object + /// Creates a new VotingResults instance pub fn new(proposal_id: String) -> Self { Self { proposal_id, @@ -319,38 +219,4 @@ impl VotingResults { 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/views/governance/proposal_detail.html b/actix_mvc_app/src/views/governance/proposal_detail.html index 6e57918..a52cb06 100644 --- a/actix_mvc_app/src/views/governance/proposal_detail.html +++ b/actix_mvc_app/src/views/governance/proposal_detail.html @@ -92,17 +92,17 @@
Voting Period
- {% if proposal.voting_starts_at and proposal.voting_ends_at %} + {% if proposal.vote_start_date and proposal.vote_end_date %}
Start Date
-
{{ proposal.voting_starts_at | date(format="%Y-%m-%d") }}
+
{{ proposal.vote_start_date | date(format="%Y-%m-%d") }}
End Date
-
{{ proposal.voting_ends_at | date(format="%Y-%m-%d") }}
+
{{ proposal.vote_end_date | date(format="%Y-%m-%d") }}
{% else %}
Not set
diff --git a/actix_mvc_app/src/views/governance/proposals.html b/actix_mvc_app/src/views/governance/proposals.html index fd23e97..f03fa50 100644 --- a/actix_mvc_app/src/views/governance/proposals.html +++ b/actix_mvc_app/src/views/governance/proposals.html @@ -106,9 +106,9 @@ {{ proposal.created_at | date(format="%Y-%m-%d") }} - {% if proposal.voting_starts_at and proposal.voting_ends_at %} - {{ proposal.voting_starts_at | date(format="%Y-%m-%d") }} to {{ - proposal.voting_ends_at | date(format="%Y-%m-%d") }} + {% if proposal.vote_start_date and proposal.vote_end_date %} + {{ proposal.vote_start_date | date(format="%Y-%m-%d") }} to {{ + proposal.vote_end_date | date(format="%Y-%m-%d") }} {% else %} Not set {% endif %} -- 2.40.1 From 7b15606da5ebd01d8c8c1181955289b5bb739c8d Mon Sep 17 00:00:00 2001 From: Mahmoud-Emad Date: Wed, 28 May 2025 09:24:56 +0300 Subject: [PATCH 18/26] refactor: Remove unnecessary debug print statements - Removed several `println!` statements from the `governance` controller and `proposals` database module to improve code cleanliness and reduce unnecessary console output. - Updated the `all_activities.html` template to use the `created_at` field instead of `timestamp` for activity dates. - Updated the `index.html` template to use the `created_at` field instead of `timestamp` for activity timestamps. - Added `#[allow(unused_assignments)]` attribute to the `create_activity` function in `proposals.rs` to suppress a potentially unnecessary warning. --- actix_mvc_app/src/controllers/governance.rs | 14 ++++---------- actix_mvc_app/src/db/proposals.rs | 1 + .../src/views/governance/all_activities.html | 2 +- actix_mvc_app/src/views/governance/index.html | 2 +- 4 files changed, 7 insertions(+), 12 deletions(-) diff --git a/actix_mvc_app/src/controllers/governance.rs b/actix_mvc_app/src/controllers/governance.rs index a8ca20b..21dbcc8 100644 --- a/actix_mvc_app/src/controllers/governance.rs +++ b/actix_mvc_app/src/controllers/governance.rs @@ -83,7 +83,6 @@ impl GovernanceController { /// Handles the governance dashboard page route pub async fn index(tmpl: web::Data, session: Session) -> Result { - println!("=============================================="); let mut ctx = tera::Context::new(); ctx.insert("active_page", "governance"); ctx.insert("active_tab", "dashboard"); @@ -100,14 +99,13 @@ impl GovernanceController { let user = Self::get_user_from_session(&session).unwrap(); ctx.insert("user", &user); - println!("=============================================="); // Get proposals from the database let proposals = match crate::db::proposals::get_proposals() { Ok(props) => { - println!( - "📋 Proposals list page: Successfully loaded {} proposals from database", - props.len() - ); + // println!( + // "📋 Proposals list page: Successfully loaded {} proposals from database", + // props.len() + // ); for (i, proposal) in props.iter().enumerate() { println!( " Proposal {}: ID={}, title={:?}, status={:?}", @@ -125,7 +123,6 @@ impl GovernanceController { vec![] } }; - println!("=============================================="); // Make a copy of proposals for statistics let proposals_for_stats = proposals.clone(); @@ -192,7 +189,6 @@ impl GovernanceController { ctx.insert("user", &user); } - println!("============== Loading proposals ================="); // Get proposals from the database let mut proposals = match get_proposals() { Ok(props) => props, @@ -202,8 +198,6 @@ impl GovernanceController { } }; - println!("proposals: {:?}", proposals); - // Filter proposals by status if provided if let Some(status_filter) = &query.status { if !status_filter.is_empty() { diff --git a/actix_mvc_app/src/db/proposals.rs b/actix_mvc_app/src/db/proposals.rs index e354c90..2f1f51b 100644 --- a/actix_mvc_app/src/db/proposals.rs +++ b/actix_mvc_app/src/db/proposals.rs @@ -189,6 +189,7 @@ pub fn submit_vote_on_proposal( 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, diff --git a/actix_mvc_app/src/views/governance/all_activities.html b/actix_mvc_app/src/views/governance/all_activities.html index ed5e048..f36ce82 100644 --- a/actix_mvc_app/src/views/governance/all_activities.html +++ b/actix_mvc_app/src/views/governance/all_activities.html @@ -54,7 +54,7 @@ - {{ activity.timestamp | date(format="%Y-%m-%d %H:%M") }} + {{ activity.created_at | date(format="%Y-%m-%d %H:%M") }} diff --git a/actix_mvc_app/src/views/governance/index.html b/actix_mvc_app/src/views/governance/index.html index f76db67..6d329a9 100644 --- a/actix_mvc_app/src/views/governance/index.html +++ b/actix_mvc_app/src/views/governance/index.html @@ -130,7 +130,7 @@
{{ activity.user }} - {{ activity.timestamp | date(format="%H:%M") }} + {{ activity.created_at | date(format="%H:%M") }}

{{ activity.action }} on {{ -- 2.40.1 From 2827cfebc96fcf4d14051b30f2f53f95c5cd1ac3 Mon Sep 17 00:00:00 2001 From: Mahmoud-Emad Date: Wed, 28 May 2025 09:29:19 +0300 Subject: [PATCH 19/26] refactor: Rename `proposals` module to `governance` The `proposals` module has been renamed to `governance` to better reflect its purpose and content. This improves code clarity and consistency. - Renamed the `proposals` module to `governance` throughout the project to reflect the broader scope of governance features. - Updated all related imports and function calls to use the new module name. --- actix_mvc_app/src/controllers/governance.rs | 12 ++++++------ actix_mvc_app/src/db/{proposals.rs => governance.rs} | 0 actix_mvc_app/src/db/mod.rs | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) rename actix_mvc_app/src/db/{proposals.rs => governance.rs} (100%) diff --git a/actix_mvc_app/src/controllers/governance.rs b/actix_mvc_app/src/controllers/governance.rs index 21dbcc8..f76954d 100644 --- a/actix_mvc_app/src/controllers/governance.rs +++ b/actix_mvc_app/src/controllers/governance.rs @@ -1,4 +1,4 @@ -use crate::db::proposals::{ +use crate::db::governance::{ self, create_activity, get_all_activities, get_proposal_by_id, get_proposals, get_recent_activities, }; @@ -100,7 +100,7 @@ impl GovernanceController { ctx.insert("user", &user); // Get proposals from the database - let proposals = match crate::db::proposals::get_proposals() { + let proposals = match crate::db::governance::get_proposals() { Ok(props) => { // println!( // "📋 Proposals list page: Successfully loaded {} proposals from database", @@ -375,7 +375,7 @@ impl GovernanceController { } else { ProposalStatus::Active }; - match proposals::create_new_proposal( + match governance::create_new_proposal( &user_id, &user_name, proposal_title, @@ -409,7 +409,7 @@ impl GovernanceController { // For now, we'll just redirect to the proposals page with a success message // Get proposals from the database - let proposals = match crate::db::proposals::get_proposals() { + let proposals = match crate::db::governance::get_proposals() { Ok(props) => { println!( "✅ Successfully loaded {} proposals from database", @@ -485,7 +485,7 @@ impl GovernanceController { }; // Submit the vote - match crate::db::proposals::submit_vote_on_proposal( + match crate::db::governance::submit_vote_on_proposal( proposal_id_u32, user_id, &form.vote_type, @@ -546,7 +546,7 @@ impl GovernanceController { 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::proposals::get_proposals() { + let proposals = match crate::db::governance::get_proposals() { Ok(props) => props, Err(e) => { ctx.insert("error", &format!("Failed to load proposals: {}", e)); diff --git a/actix_mvc_app/src/db/proposals.rs b/actix_mvc_app/src/db/governance.rs similarity index 100% rename from actix_mvc_app/src/db/proposals.rs rename to actix_mvc_app/src/db/governance.rs diff --git a/actix_mvc_app/src/db/mod.rs b/actix_mvc_app/src/db/mod.rs index 6def13b..cfe9423 100644 --- a/actix_mvc_app/src/db/mod.rs +++ b/actix_mvc_app/src/db/mod.rs @@ -1,2 +1,2 @@ pub mod db; -pub mod proposals; +pub mod governance; -- 2.40.1 From d815d9d365257e71ac66813babab7cb2338f543b Mon Sep 17 00:00:00 2001 From: Mahmoud-Emad Date: Wed, 28 May 2025 10:43:02 +0300 Subject: [PATCH 20/26] feat: Add custom Tera filters for date/time formatting - Add three new Tera filters: `format_hour`, `extract_hour`, and `format_time` for flexible date/time formatting in templates. - Improve template flexibility and maintainability by allowing customizable date/time display. - Enhance the user experience with more precise date/time rendering. --- actix_mvc_app/src/db/calendar.rs | 0 actix_mvc_app/src/utils/mod.rs | 70 +- actix_mvc_app/src/views/calendar/index.html | 700 +++++++++++++++++--- 3 files changed, 671 insertions(+), 99 deletions(-) create mode 100644 actix_mvc_app/src/db/calendar.rs diff --git a/actix_mvc_app/src/db/calendar.rs b/actix_mvc_app/src/db/calendar.rs new file mode 100644 index 0000000..e69de29 diff --git a/actix_mvc_app/src/utils/mod.rs b/actix_mvc_app/src/utils/mod.rs index 55961fe..dbd8118 100644 --- a/actix_mvc_app/src/utils/mod.rs +++ b/actix_mvc_app/src/utils/mod.rs @@ -26,11 +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 @@ -140,6 +145,69 @@ impl Function for LocalTimeFunction { } } +/// 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 { diff --git a/actix_mvc_app/src/views/calendar/index.html b/actix_mvc_app/src/views/calendar/index.html index fcca452..1b98ca1 100644 --- a/actix_mvc_app/src/views/calendar/index.html +++ b/actix_mvc_app/src/views/calendar/index.html @@ -4,129 +4,633 @@ {% block content %}

-

Calendar

- -

View Mode: {{ view_mode }}

-

Current Date: {{ current_date }}

- -
-
- Day - Month - Year + +
+
+
+
+

Calendar

+

Manage your events and schedule

+
+
+ +
+
- - Create New Event -
- + + +
+
+
+ +
+ {{ 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 View: {{ month_name }} {{ current_year }}

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

+ {{ 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 %} + {% endfor %} + {% if day.events|length > 2 %} +
+{{ day.events|length - 2 }} more
+ {% endif %} + + {% endif %} + {% endif %} + {% endif %} + + {% 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 %} + {% if loop.index <= 2 %}
+ {{ event.title }} +
{% endif %} - {% endif %} -
- {% elif view_mode == "year" %} -

Year View: {{ current_year }}

- +
+
+
+
+{% elif view_mode == "year" %} + +
+
+

+ Year {{ current_year }} +

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

Events: {{ month.events|length }}

+
+
+
+
{{ 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 %}
- {% elif view_mode == "day" %} -

Day View: {{ current_date }}

- -
-
- All Day Events +
+
+{% 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) %} +
+
+
+ {{ hour|format_hour }}:00
-
+
{% 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

+ {% for event in events %} + {% if not event.all_day %} + {% set start_hour = event.start_time|extract_hour %} + {% if start_hour == hour|string %} +
+
{{ event.title }}
+

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

+

{{ event.description }}

+
+ {% endif %} + {% endif %} + {% endfor %} {% endif %}
- -
- {% for hour in range(start=0, end=24) %} -
-
-
- {{ "%02d"|format(value=hour) }}:00 +
+ {% endfor %} +
+{% endif %} + + + {% block extra_css %} + +{% endblock %} + +{% block extra_js %} + {% endblock %} {% endblock %} \ No newline at end of file -- 2.40.1 From 58d1cde1cefa69b04af5b2d2129b568140d632b7 Mon Sep 17 00:00:00 2001 From: Mahmoud-Emad Date: Wed, 28 May 2025 15:48:54 +0300 Subject: [PATCH 21/26] feat: Migrate calendar functionality to a database - Replaced Redis-based calendar with a database-backed solution - Implemented database models for calendars and events - Improved error handling and logging for database interactions - Added new database functions for calendar management - Updated calendar views to reflect the database changes - Enhanced event creation and deletion processes - Refined date/time handling for better consistency --- actix_mvc_app/src/controllers/calendar.rs | 453 +++++++++++++----- actix_mvc_app/src/db/calendar.rs | 360 ++++++++++++++ actix_mvc_app/src/db/mod.rs | 1 + actix_mvc_app/src/views/calendar/index.html | 239 +++++++-- .../src/views/calendar/new_event.html | 110 ++++- 5 files changed, 972 insertions(+), 191 deletions(-) diff --git a/actix_mvc_app/src/controllers/calendar.rs b/actix_mvc_app/src/controllers/calendar.rs index e4bfbc6..ee99d9e 100644 --- a/actix_mvc_app/src/controllers/calendar.rs +++ b/actix_mvc_app/src/controllers/calendar.rs @@ -1,12 +1,16 @@ -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::db::calendar::{ + add_event_to_calendar, create_new_event, delete_event, get_events, get_or_create_user_calendar, +}; use crate::models::{CalendarEvent, CalendarViewMode}; -use crate::utils::{RedisCalendarService, render_template}; +use crate::utils::render_template; +use heromodels_core::Model; /// Controller for handling calendar-related routes pub struct CalendarController; @@ -14,9 +18,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 +33,186 @@ 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 and convert to CalendarEvent format + db_events + .into_iter() + .filter(|event| { + // Event overlaps with the date range + event.start_time < end_date && event.end_time > start_date + }) + .map(|event| CalendarEvent { + id: event.get_id().to_string(), + title: event.title.clone(), + description: event.description.clone().unwrap_or_default(), + start_time: event.start_time, + end_time: event.end_time, + color: event.color.clone().unwrap_or_else(|| "#4285F4".to_string()), + all_day: event.all_day, + user_id: event.created_by.map(|id| id.to_string()), + }) + .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 +221,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 +258,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 +513,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 +536,7 @@ impl CalendarController { _ => "", } } - + /// Returns the name of the day fn day_name(day: u32) -> &'static str { match day { @@ -397,4 +584,4 @@ struct CalendarMonth { month: u32, name: String, events: Vec, -} \ No newline at end of file +} diff --git a/actix_mvc_app/src/db/calendar.rs b/actix_mvc_app/src/db/calendar.rs index e69de29..b71458c 100644 --- a/actix_mvc_app/src/db/calendar.rs +++ 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, EventStatus}, +}; + +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/mod.rs b/actix_mvc_app/src/db/mod.rs index cfe9423..0b27adf 100644 --- a/actix_mvc_app/src/db/mod.rs +++ b/actix_mvc_app/src/db/mod.rs @@ -1,2 +1,3 @@ +pub mod calendar; pub mod db; pub mod governance; diff --git a/actix_mvc_app/src/views/calendar/index.html b/actix_mvc_app/src/views/calendar/index.html index 1b98ca1..1bc8aed 100644 --- a/actix_mvc_app/src/views/calendar/index.html +++ b/actix_mvc_app/src/views/calendar/index.html @@ -13,8 +13,7 @@

Manage your events and schedule

-
@@ -245,6 +244,14 @@